20211125のReactに関する記事は6件です。

React Hook Form + Material UI + yup + TypeScript で良い感じのフォームを作成する

はじめに React Hook Form、Material UI、yup を組み合わせたフォームを作成する方法をメモしておきます。 始め方シリーズ 良い感じの環境構築についても記事を書いているので、よければこちらもご覧ください。 バージョン package.json { "dependencies": { "@hookform/resolvers": "^2.8.3", "@mui/material": "^5.1.1", "react-hook-form": "^7.20.2", "yup": "^0.32.11" }, } コード Form.tsx import { yupResolver } from "@hookform/resolvers/yup/dist/yup"; import { Button, Checkbox, TextField } from "@mui/material"; import { Controller, useForm } from "react-hook-form"; import * as yup from "yup"; interface FormValues { hoge: string; num: number; checked: boolean; } const schema: yup.SchemaOf<FormValues> = yup.object({ hoge: yup.string().required("必須だよ"), num: yup.number().required("必須だよ").min(20, "20以上にしてください"), checked: yup.boolean().required("必須だよ").isTrue("チェックしてください"), }); const Form = (): JSX.Element => { const { handleSubmit, formState: { errors }, control, } = useForm<FormValues>({ defaultValues: { hoge: "", num: 1, checked: true, }, resolver: yupResolver(schema), }); return ( <form onSubmit={handleSubmit((data) => console.log(data))}> <Controller name="hoge" control={control} render={({ field }): JSX.Element => ( <TextField {...field} type="text" label="hoge のラベル" multiline rows={8} variant="filled" fullWidth error={"hoge" in errors} helperText={errors.hoge?.message} /> )} /> <Controller name="num" control={control} render={({ field }): JSX.Element => ( <TextField {...field} type="number" label="num のラベル" variant="filled" fullWidth error={"num" in errors} helperText={errors.num?.message} /> )} /> <Controller name="checked" control={control} render={({ field }): JSX.Element => ( <Checkbox onChange={(e): void => field.onChange(e.target.checked)} checked={field.value} /> )} /> <Button type="submit" variant="contained"> submit </Button> </form> ); }; export default Form; 参考にさせていただいたサイト 公式ドキュメント 公式の examples 中でも Controller (MUI, React-Select and etc)が参考になります こちらは register を使う方法 公式が Contoller を使用していたので、そちらに合わせました。 yup の import でエラーが出たので、この回答の対応をしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactでRedux Thunkについて理解する

はじめに 前回Reduxについての記事[1]を書きましたが、非同期処理についてはあまり触れてきませんでした。実務で使用する必要が出てきたのでRedux Thunkについてまとめていこうと思います。 前提 Reduxが使える Reactが使える TypeScriptが使える 非同期処理が何か分かる Redux Thunk Reduxにおける非同期処理のためのミドルウェアです。Redux Style Guide優先度Cのルールにも非同期ロジックにRedux Thunkを使うということが書かれている公式推奨の非同期用ミドルウェアです(賛否両論ありますが)。 Redux Thunkの概念 これら記事[2][3]を参考に解説をしていきます。まず、普通に非同期処理の実装を考える場合を考えます。その場合コンポーネントまたはhooksで非同期処理を行い、actionをdispatchをするという流れになります。それに対し、非同期処理もまとめてactionで処理をしたいと考えた場合問題が発生します。Reduxにおいてdispatchするactionはtypeとpayloadを持つオブジェクトでなければいけません。しかし、async関数でAction Creatorを実装した場合、Promiseオブジェクトが返されてしまうため、actionに非同期処理を実装出来ません。 それを解決するミドルウェアがRedux Thunkです。Thunkというのは関数を使用して遅延を行う概念です。下に概念的なThunkの例を載せておきます。 let thunk = () => 1 + 2 // この時点では 1 + 2 は評価されない thunk() // 3 // Thunk関数が呼び出されたタイミングで 1 + 2 が評価される [3]より引用 要するにThunkというのは遅延のための関数であり、Redux ThunkはこのThunkの概念をReduxで利用出来るようにするミドルウェアです。Redux Thunkを導入することでdispatchにactionだけでなく、非同期関数も渡すことが出来ます。つまり、dipatchされた時の動作は2パターンあります。actionがdispatchされた場合は通常通りにreducerの実行します。関数がdispatchされた場合はRedux Thunkで処理が行われ、Redux Thunkからactionがdispatchされます。下記にReduxの非同期処理のデータフローを載せておきます。 [4]より引用 ReduxにおいてThunk関数は同期、非同期を問わず、任意のロジックを含むことが出来、いつでもdispatchやgetStateを呼び出すことが出来ます。また、引数としてRedux StoreのdispatchメソッドとgetStateメソッドの2つの引数を受け取れます。またThunk関数はactionともみなせるので、ここからはThunk Actionと呼んでいくことにします。 Thunk Actionはdispatchとstateを引数に持つ関数であるので、Thunk Actionを返すAction Creatorは関数を返します。Thunk Action内に非同期処理を実装することで、Redux内で非同期処理を扱うことが出来ます。Thunk Actionの型定義では4つの引数が存在し、第一引数がactionの戻り値の型、第二引数がstateの型、第三引数がThunk Actionのdispatch・getState以外の引数の型、第四引数がactionの型となっています。下記に例を載せておきます。 export const getUsers = (): ThunkAction< void, RootState, unknown, AnyAction > => { return async (dispatch: AppThunkDispatch) => { const res = await fetch("https://jsonplaceholder.typicode.com/users"); const data: userDataType[] = await res.json(); dispatch(setUsers(data)); }; }; 実際のコードで確認する JSONSPlaceholderを利用してユーザーデータを取ってくるAPI処理を考えます。 1 storeにミドルウェアの追加を行う これはやらなくても動いたのですが、Redux ToolKitを導入している影響なのかなぜ動くのかよく分かりません。調べた感じ要るんじゃないかなあと思っているんですが、どなたか有識者教えてください。 export const store = configureStore({ reducer: { ・・・ }, middleware: [thunk], }); 2 Thunk Action Creatorの作成 userDataというstateを作成し、データをフェッチしてきてstateにセットするというThunk Actionを返すThunk Action Creatorを作成しています。あとなぜか、stateにpayloadを直接突っ込むと再レンダリングされませんでした。調べた感じimmutabilityの問題っぽいのですが、reducerの中は内部的にimmerがあるので問題ないはずなのになんでなのか分かりません。Thunk Action内ではimmerが効かないとかなんでしょうか。とりあえずreturnすることで動作しましたが、後で要検証です。 import { AnyAction } from "redux"; import { createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit"; import { AppThunkDispatch, RootState } from "../../assets/type/reduxType"; import { useAppSelector } from "../../hooks/useAppSelector"; export type userDataType = { id: number; name: string; username: string; email: string; address: { street: string; suite: string; city: string; zipcode: string; geo: { lat: string; lng: string; }; }; phone: string; website: string; company: { name: string; catchPhrase: string; bs: string; }; }; const initialState: userDataType[] = []; export const userDataSlice = createSlice({ name: "userData", initialState: initialState, reducers: { setUsers: (state, action: PayloadAction<userDataType[]>) => { // state = action.payload; だめ? return action.payload; }, }, }); export const getUsers = (): ThunkAction< void, RootState, unknown, AnyAction > => { return async (dispatch: AppThunkDispatch) => { const res = await fetch("https://jsonplaceholder.typicode.com/users"); const data: userDataType[] = await res.json(); dispatch(setUsers(data)); }; }; //action export const { setUsers } = userDataSlice.actions; //selector export const useUserDataSelector = () => { const userData = useAppSelector((state: RootState) => state.userData); return { userData }; }; //reducer export const userDataReducer = userDataSlice.reducer; 3 useDispatchの型変更 dispatchをthunkにも対応するように型変更しないとエラーが出ます[5]。 export type AppThunkDispatch = ThunkDispatch<RootState, void, AnyAction>; import { AppThunkDispatch } from "../assets/type/reduxType"; import { useDispatch } from "react-redux"; export const useAppDispatch = () => { const dispatch = useDispatch<AppThunkDispatch>(); return { dispatch }; }; 4 コンポーネントでのThunk Action Creatorの呼び出し コンポネート側の記述はこのようになります。 import { VFC } from "react"; import { useAppDispatch } from "../../hooks/useAppDispatch"; import { getUsers, setUsers, useUserDataSelector, } from "../../store/slices/userData"; export const ReduxThunkPage1: VFC = () => { const { userData } = useUserDataSelector(); const { dispatch } = useAppDispatch(); return ( <div> <button onClick={() => dispatch(getUsers())}>フェッチデータ</button> <button onClick={() => dispatch(setUsers([]))}>データクリア</button> {userData && userData.map((user, index) => <div key={index}>{user.name}</div>)} </div> ); }; おわりに やっている事自体はそんなに難しくなかったのですが、公式ドキュメントが読みづらすぎて理解するのに時間がかかってしまいました。Redux ToolKitを使用している場合はcreateAsyncThunkを使用すれば良いのかなと思うのですが、既存プロジェクトだとRedux Toolkitを使っていないものも多いので、Redux Thunkと出会う機会は少なくないでしょう。今度はRecoilでReduxを代替出来ないかについて考えていきたいと思います。 参考文献 [1]:今から始めるRedux x React x TypeScript [2]:Writing Logic with Thunks [3]:Todoアプリで理解するRedux Thunkによる非同期処理の実装方法 [4]:Redux Async Data Flow [5]:Typing Additional Redux Logic [6]:typescriptでuseDispatchでRedux Thunkのthenが型エラーになる時の対応 [7]:Redux入門者向け初めてのRedux ToolkitとRedux Thunkの非同期処理
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

モダンフロントとLaravelでCRUD処理をやってみる

はじめに 【マイスター・ギルド】本物の Advent Calendar 2021の4日目の記事です。 他にもROCKな記事ばっかりなので要チェック!!! 本記事は筆者が以下の2つの記事を自身の学習用にまとめたものです。 大変参考にさせていただきました。ありがとうございます。 『Laravel CRUD』 『LaravelでCRUD』 モダンフロントとLaravelの組み合わせを想定しているので、bladeは使いません。 フロント部分についてはコードは載せますが、詳細は触れません。 Laravel側へリクエスト投げていれば何でも大丈夫です。今回はReactとaxiosを用います。 執筆の経緯 LaravelでCRUDをやってみたいが、bladeを使っている記事ばかり。 モダンフロントとLaravelの記事が少ない・・・。 なかったら作れ!(ハック精神) 想定読者 前回の記事で環境構築された方。 記事の通りに環境構築された場合はバージョンは以下になっているはず。 PHP : 7.3.32 Composer : 2.0.14 Laravel : 6.20.41 Laravel初心者の方。 LaravelおよびMVCモデルの基礎について簡単に理解している前提で話を進めます。 今回のゴール 簡単なCRUD処理を実装する。 タスク管理アプリを作る。 実行環境 PC : MacBookAir(M1, 2020) OS : macOS Big Sur11.4 チップ : Apple M1 メモリ : 16GB DockerDesktop : 3.5.2 Docker : 20.10.7 目次 何を作るか Laravelの前準備 処理のイメージ Controllerを作成する。 web.phpを編集し、ルーティングを設定する。 .envを編集する Modelおよびmigrationを作成し、テーブルを作成する Seederを作成して実行し、サンプルレコードを作成する フロントエンドの完成形 【要件1の実装】データベースからレコードを読み込む(Read) 【要件2の実装】データベースへレコードを挿入する(Create) 【要件3の実装】データベースの情報を更新する(Update) 【要件4の実装】データベースの情報を削除する(Delete) リポジトリ 何を作るか 簡単なタスク管理アプリを作成します。簡単な要件は以下の通り。 【要件1】ページ遷移時にデータベースから既存のタスクがfetchされている。 【要件2】タスクを追加することができる。 【要件3】「変更する」ボタンを押下すると状況が「作業中」と「完了」のトグルで入れ替わる。 【要件4】「削除する」ボタンを押下するとタスクが削除される。もちろんDBからも消えている。 Laravelの前準備 Laravelの処理の簡単なイメージ とりあえずModel、View、Controller、Route、DBが分かっていればOK。小売店と卸問屋で例えてみましょう。 View ブラウザ株式会社という小売店のスタッフ。お客さんに画面という情報を提供している。 今回はここがbladeじゃなくてReact。 Route サーバー株式会社という卸問屋の電話番。 Controller サーバー株式会社の営業マン。 Model サーバー株式会社の在庫管理の部署。 DB サーバー株式会社の倉庫。 イメージ Viewから商品問い合わせの電話が入った。 Routeが電話を受けて、担当営業マンのControllerに繋ぐ。 ControllerがModelへ在庫を確認。 ModelはDBへ在庫を確認、Viewへ発送する。 ControllerがViewへ納期を伝える。 イメージ違ってたらご指摘いただけると幸いです。 Controllerを作成する。 Controllerを作成する 中身は後ほど実装するので、とりあえず作成だけしておく。 backendコンテナの中 php artisan make:controller TodoController web.phpを編集し、ルーティングを設定する。 以下の通り設定。 これでpublic/fetch_todosにgetメソッドでリクエストが来たら、TodoControllerのfetchTodosメソッドを実行するよう設定ができている。 前述の通りcontrollerの中身は後ほど実装します。 routes/web.php Route::get('fetch_todos', 'TodoController@fetchTodos'); .envファイルを編集する Laravelプロジェクト直下にある.envファイルを以下の通り修正。 dockerは各コンテナ間のネットワークも自動で作成をしてくれている。 そのためLaravel(backendコンテナ)からMySQL(dbコンテナ)へサービス名でアクセスできるようになっている。 つまり「DB_HOST」の値は「db」でOK。 .env(一部抜粋) DB_CONNECTION=mysql // dockerで環境構築した場合は、docker-compose.ymlデータベースのコンテナのサービス名を入力する。 DB_HOST=db DB_PORT=3306 // 任意のデータベース名。先にDBコンテナに入ってデータベースだけは作成しておくこと。 DB_DATABASE=tasuku // rootユーザーは初期設定で存在しているので、このままでOK。 DB_USERNAME=root // docker-compose.ymlで設定したパスワードを入力。 DB_PASSWORD=pass Modelおよびmigrationを作成し、テーブルを作成する modelファイルとmigrationファイルを作成する migrationファイルとは簡易に書け、なおかつ複数実行可能なSQL文の集まりのようなものです。 -mオプションをつけることで、modelと一緒にmigrationも作成されます。 今回はタスクを格納するtodosテーブルのみ作成します。 model名は単数形にすること。(migrationファイルは複数形名で自動生成されている) ってことはmodelはきっとテーブルの各レコードってことなんだろうか。 backendコンテナ内 php artisan make:model Models/Todo -m migrationファイルを編集 ->nullable(false)のような書き方をすることで、該当のカラムにオプションを付与します。 私はいつも頭の中でSQLを思い浮かべてからどう書くのか検索したりしてます。 database/migrations/(作成時の日時)_create_todos_table.php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateTodosTable extends Migration { public function up() { Schema::create('todos', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name', 255)->nullable(false); $table->string('status', 255)->nullable(false); $table->timestamps(); }); } public function down() { Schema::dropIfExists('todos'); } } migrationを実行 事前にdbコンテナ内で.envで指定したデータベースを作成しておかないとエラーになるので注意。 実行したらdbコンテナ内でテーブルができているか確認。 SQLポチポチ叩かなくて良いので便利! backendコンテナ内 php artisan migrate Seederを作成して実行し、サンプルレコードを作成する Seederファイルを作成する Seederファイルとはサンプルのデータを作成できるファイル。 さっきのmigrationがcreate table文の簡易版とすると、seederはinsert文の簡易版。 backendコンテナ内 php artisan make:seeder TodosTableSeeder Seederファイルを編集 適当にファイルを作成する。 id,created_at,updated_atはSeederには入力不要。 初期だとDBがimportされていないので、追記すること。 database/seeds/TodosTableSeeder.php use Illuminate\Support\Facades\DB; use Illuminate\Database\Seeder; class TodosTableSeeder extends Seeder { public function run() { DB::table('todos')->insert( [ [ 'name' => '買い物', 'status' => '作業中' ], [ 'name' => '料理', 'status' => '作業中' ], [ 'name' => '洗濯', 'status' => '作業中' ] ] ); } } DatabaseSeederへの登録 後述のseedコマンドを実行すると、このファイルが実行される。 つまりこのファイルに先ほど作成したSeederを記載しないと意味なし。 database/seeds/DatabaseSeeder.php use Illuminate\Database\Seeder; class DatabaseSeeder extends Seeder { /** * Seed the application's database. * * @return void */ public function run() { $this->call(TodosTableSeeder::class); } } Seederファイルの実行 dbコンテナにデータが入っていることを確認。 backendコンテナ内 php artisan db:seed フロントエンドの完成形 一部抜粋です。完成系は本記事の終わりのgithub参照。何やっているか雰囲気だけ掴んでもらえれば。 Reactで書いてますが、画面表示時にLaravelへgetメソッド投げれたら何でも良いです。 Vue.jsの場合はcreated()にfetchTodosメソッド、それ以外のメソッドはv-onで制御すればOKかと思います。 バニラJSの場合はaddEventListener駆使してください。 Main.tsx import { useState, useEffect, ChangeEvent } from 'react' import { client, postMethod } from './lib/axios' import { createURLSearchParams } from './utils' interface TodoType { id: string name: string status: string } const WORK_IN_PROGRESS = '作業中' const DONE = '完了' export const Main = () => { const [todos, setTodos] = useState<TodoType[]>([]) const [todoName, setTodoName] = useState('') const bindTodoNameValue = (event: ChangeEvent<HTMLInputElement>) => { setTodoName(event.target.value) } // 要件1 const fetchTodos = async () => { const { data } = await client.get<TodoType[]>('/fetch_todos') setTodos(data) } useEffect(() => { fetchTodos() }, []) // 要件2 const pushTodo = () => { const params = createURLSearchParams<TodoType>([ ['name', todoName], ['status', WORK_IN_PROGRESS], ]) postMethod('push_todo', params).then((_response) => fetchTodos()) } // 要件3 const changeStatus = (id: string, status: string) => { const statusParam = status === WORK_IN_PROGRESS ? DONE : WORK_IN_PROGRESS const params = createURLSearchParams<TodoType>([ ['id', id], ['status', statusParam], ]) postMethod('change_status', params).then((_response) => fetchTodos()) } // 要件4 const deleteTodo = (id: string) => { const params = createURLSearchParams<TodoType>([['id', id]]) postMethod('delete_todo', params).then((_response) => fetchTodos()) } return ( <> <p>タスクを追加する</p> <input onChange={bindTodoNameValue} type="text" /> <button onClick={pushTodo}>追加する</button> <ul> {todos.length > 0 && todos.map(({ id, name, status }, index) => { return ( <li key={id}> <p>Index:{index + 1}</p> <p>タスク名:{name}</p> <p>状況:{status}</p> <button onClick={() => changeStatus(id, status)}> 変更する </button> <button onClick={() => deleteTodo(id)}>削除する</button> </li> ) })} </ul> </> ) } 【要件1の実装】データベースからレコードを読み込む(Read) 処理が簡単なので、まずはReadからやります。 TodoControllerを以下の通り実装。 header("Access-Control-Allow-Origin: *")を付けないとCSRFのエラーが出ます。 簡単に言うと「違うドメインから通信来ても受け入れるよ」という設定です。 本来はちゃんとした設定をしないといけないんですが、今回は省略します。 migrationの時と似たような書き方でselect文を書いているイメージです。 json形式に変換してレスポンスを返します。 Todo::select()のようにModelを利用してDBへアクセスしていますが、DBから直接取ってくる実装方法もあります。 役割を考えると、Modelを利用するべきではと考えています。 TodoController.php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class TodoController extends Controller { public function fetchTodos() { header("Access-Control-Allow-Origin: *"); $todos = Todo::select('id' ,'name', 'status')->get(); return json_decode($todos); } } 画面をリロードしてみて、きちんとfetchできているか確認。 Chromeのdeveloperツールでも確認。200番返ってきてますね。 【要件2の実装】データベースへレコードを挿入する(Create) web.phpを編集してルーティングを追加 クライアント側から情報を受け取る時は大事な情報が入っている場合もあるでしょうから、post通信にします。 ちなみにそのままaxiosでLaravelにPOSTすると、エラーコード419が返ってきます。 これはCSRFトークンが埋め込まれていないためのエラーです。 app/Http/Kernel.phpの$middlewareGroupsの\App\Http\Middleware\VerifyCsrfToken::classをコメントアウトすれば一旦解決。 根本解決するにはリクエスト内にCSRFトークンを埋め込む必要がある。(勉強中) bladeを使用する場合はとても簡単に解決可能ですが、Laravelでフロント作る気になれなかったので今回は割愛。 routes/web.php <?php Route::get('fetch_todos', 'TodoController@fetchTodos'); // 追加 Route::post('push_todo', 'TodoController@pushTodos'); TodoControllerを以下の通り実装 フロント側から受け取った情報は$request内に格納されている。 $todo = new Todo()でModelの新しい営業担当を呼び出したイメージ。 $requst->input('name')はバニラPHPで言えば、$_POST['name']というところ。 $todo->save()でDBに格納できる。 TodoController.php <?php namespace App\Http\Controllers; // 追加 use App\Models\Todo; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class TodoController extends Controller { public function fetchTodos() { header("Access-Control-Allow-Origin: *"); $todos = Todo::select('id' ,'name', 'status')->get(); return json_decode($todos); }   //追加 public function pushTodo(Request $request) { header("Access-Control-Allow-Origin: *"); $todo = new Todo(); $todo->name = $request->input('name'); $todo->status = $request->input('status'); $todo->save(); } } フロント側から適当にタスクを追加する。 200番が返ってきていることを確認。Laravel側で何も返していないので、dataは空でOK。 リロードしても今追加したタスクが消えないことを確認。 【要件3の実装】データベースの情報を更新する(Update) web.phpを編集してルーティングを追加 routes/web.php <?php Route::get('fetch_todos', 'TodoController@fetchTodos'); Route::post('push_todo', 'TodoController@pushTodo'); // 追加 Route::post('change_status', 'TodoController@changeStatus'); TodoControllerを以下の通り実装 $id = (int) $request->input('id')でフロント側からきているidはstring型なので、int型へ型キャスト。 $todo = Todo::find($id)でTodoModelへ「$idを満たすレコードを頂戴」と言っている。 今回はstatusだけを変更したいので、上書きして保存。 TodoController.php <?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class TodoController extends Controller { public function fetchTodos() { header("Access-Control-Allow-Origin: *"); $todos = Todo::select('id' ,'name', 'status')->get(); return json_decode($todos); } public function pushTodo(Request $request) { header("Access-Control-Allow-Origin: *"); $todo = new Todo(); $todo->name = $request->input('name'); $todo->status = $request->input('status'); $todo->save(); } // 追加 public function changeStatus(Request $request) { header("Access-Control-Allow-Origin: *"); $id = (int) $request->input('id'); $todo = Todo::find($id); $todo->status = $request->input('status'); $todo->save(); } } フロント側で「変更ボタン」を押下して、「状況」が変わることを確認。 200番が出ていることも確認。 リロードしても「状況」が「完了」のままだと確認。 【要件4の実装】データベースの情報を削除する(Delete) web.phpを編集してルーティングを追加 routes/web.php <?php Route::get('fetch_todos', 'TodoController@fetchTodos'); Route::post('push_todo', 'TodoController@pushTodo'); Route::post('change_status', 'TodoController@changeStatus'); // 追加 Route::post('delete_todo', 'TodoController@deleteTodo'); TodoControllerを以下の通り実装 delete()でレコード削除。 TodoController.php <?php namespace App\Http\Controllers; use App\Models\Todo; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class TodoController extends Controller { public function fetchTodos() { header("Access-Control-Allow-Origin: *"); $todos = Todo::select('id' ,'name', 'status')->get(); return json_decode($todos); } public function pushTodo(Request $request) { header("Access-Control-Allow-Origin: *"); $todo = new Todo(); $todo->name = $request->input('name'); $todo->status = $request->input('status'); $todo->save(); } public function changeStatus(Request $request) { header("Access-Control-Allow-Origin: *"); $id = (int) $request->input('id'); $todo = Todo::find($id); $todo->status = $request->input('status'); $todo->save(); } // 追加 public function deleteTodo(Request $request) { header("Access-Control-Allow-Origin: *"); $id = (int) $request->input('id'); $todo = Todo::find($id); $todo->delete(); } } フロント側から適当にタスクを削除。 今回は「買い物」を削除してみた。 200番が返ってくることを確認。 リポジトリ https://github.com/chillout2san/laravel_crud パイセンがガッツリレビューしてくれてプルリクまで出してくれました。ギザ優しす。 そんなパイセンのアドカレ記事はこちらです→FastAPI + Reactでフルスタックアプリを作成する 終わりに 今までfirebaseしか触ったことがなかったので、バックエンド触るのは新鮮だった。 API組むの楽しすぎてハマりそう。 だがやっぱりbladeは好きになれなかった。 Laravelはドキュメントが非常に充実していてありがたい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

react-router-dom v6でstorybookを使用するときの注意

エラーソース Material UI(MUI)を使用 HeaderLink/index.tsx import Link from "@mui/material/Link"; import { Link as RouterLink } from "react-router-dom"; type HeaderLinkProps = { href: string; title: string; onClickLink?: () => void; }; const HeaderLink: React.VFC<HeaderLinkProps> = ({ title, href, onClickLink, }) => { return ( <Link component={RouterLink} to={href} onClick={() => { onClickLink?.(); }} sx={{ color: "#9d9d9d", position: "relative", textDecoration: "none", "::after": { position: "absolute", left: 0, content: '""', width: "100%", height: "2px", background: "#9d9d9d", bottom: "-1px", transform: "scale(0, 1)", transformOrigin: "center top", transition: "transform 0.3s", }, ":hover": { "::after": { transform: "scale(1, 1)", }, }, }} > {title} </Link> ); }; export default HeaderLink; HeaderLink/index.stories.tsx import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import HeaderLink from "."; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export export default { title: "Atoms/HeaderLink", component: HeaderLink, } as ComponentMeta<typeof HeaderLink>; // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args const Template: ComponentStory<typeof HeaderLink> = (args) => ( <HeaderLink {...args} /> ); export const Normal = Template.bind({}); Normal.args = { title: "テストリンク", href: "#", }; 修正後 HeaderLink/index.stories.tsx import React from "react"; import { ComponentStory, ComponentMeta } from "@storybook/react"; import HeaderLink from "."; import { MemoryRouter } from "react-router-dom"; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export export default { title: "Atoms/HeaderLink", component: HeaderLink, decorators: [ (Story) => ( <MemoryRouter> <Story /> </MemoryRouter> ), ], } as ComponentMeta<typeof HeaderLink>; // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args const Template: ComponentStory<typeof HeaderLink> = (args) => ( <HeaderLink {...args} /> ); export const Normal = Template.bind({}); Normal.args = { title: "テストリンク", href: "#", };
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

開発効率を上げる為にcommit時に自動でprettierとlintを走らせる

huskyとは? Git hooksを簡単にできるツールです。 commit、push時にscriptを走らせることができます。 Github: https://github.com/typicode/husky lint-stagetとは? ステージング済みのファイルに対して、特定の処理が走らせることができるツールです。 Github: https://github.com/okonet/lint-staged 実装 まず、開発プロジェクトを用意しましょう。 今回はReactで行います。 npx create-react-app プロジェクト名 プロジェクトを立ち上げた後、最低限必要なpackageをインストールします。 (他のlintなど必要な場合プロジェクトによって変更してください。今回はprettierとeslintの設定に関しては触れません。) ・ Eslint ・ prettier ・ husky ・ lint-staged npm i -D eslint prettier husky lint-staged 必要な設定を行っていきます。 "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --fix \"src/**/*.{ts,tsx}\"", "prettier --write \"src/**/*.{ts,tsx,css}\"" ] }, ※設定内容に関しては、プロジェクトごとに対応した変更をしてください。 Git hooksを有効化していきます。 npx husky install .huskyディレクトリが作成されます。 Githubのリポジトリからcloneする際にGithooksを有効化する為に以下のスクリプトを作成します。 "scripts": { "prepare": "husky install" "lint-staged":"lint-staged" } コミットした時に実行されるhooksを用意します。 npx husky add .husky/pre-commit "npm run lint-staged" 実行後に.huskyフォルダ内に.husky/pre-commitファイルが生成されます。(このファイルに記述している内容が走ります。必要があれば他のscriptも追加してください。) あとは、commitした時に実行されていれば完了です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Mac+React+Jestで、watchmanが動かない場合の対応

はじめに Mac + React + Jestでテストが動作しない場合の対応方法。 現象 npm test でテストを実行しようとすると以下のようなメッセージが表示される。 Watchman crawl failed. Retrying once with node crawler. Usually this happens when watchman isn't running. Create an empty `.watchmanconfig` file in your project's root folder or initialize a git or hg repository in your project. Error: Watchman error: std::__1::system_error: open: /Users/xxxxx/Documents/xxxxx/src: Operation not permitted. Make sure watchman is running for this project. See https://facebook.github.io/watchman/docs/troubleshooting. 対策 メッセージにあるように、プロジェクトディレクトリに.watchmanconfigを作成する。 →改善せず ネットで調べると、どうやらwatchmanがプロジェクトディレクトリにアクセスできないことが原因のようす。 まずは、watchmanがインストールされているかを確認。 brew list watchman /usr/local/Cellar/watchman/2021.10.04.00/bin/watchman /usr/local/Cellar/watchman/2021.10.04.00/bin/watchman-diag /usr/local/Cellar/watchman/2021.10.04.00/bin/watchman-make /usr/local/Cellar/watchman/2021.10.04.00/bin/watchman-replicate-subscription /usr/local/Cellar/watchman/2021.10.04.00/bin/watchman-wait /usr/local/Cellar/watchman/2021.10.04.00/bin/watchmanctl /usr/local/Cellar/watchman/2021.10.04.00/lib/python3.9/ (13 files) うん。インストールはされている。 次のMacのシステム環境設定を変更する。 システム環境設定を起動し、「セキュリティとプライバシー」を選択。 左側のメニューから「フルディスクアクセス」を選択。 左下の鍵アイコンをクリックして、パスワードを入力。 右側のリストの「+」アイコンを押す。 ダイヤログが開く。 右上の検索ウィンドウで、command + shift + .(ピリオド)を入力。←このオペレーションがわからず、すごい苦労した 先程のbrewの結果を確認し、watchmanを探す(私は/usr/local/Cellar/watchman/2021.10.04.00/binにあった)。 「開く」を押して選択 チェックが入っていることを確認して、左下のロックをクリック。 では再びnpm testを実行。 No tests found related to files changed since last commit. Press `a` to run all tests, or run Jest with `--watchAll`. Watch Usage › Press a to run all tests. › Press f to run only failed tests. › Press q to quit watch mode. › Press p to filter by a filename regex pattern. › Press t to filter by a test name regex pattern. › Press Enter to trigger a test run. 解決ー!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む