React からだいぶ離れている間に、16.8 で Hooks が出て、React を使った Web アプリ開発のパラダイムがかなり変わったようだ。Hooks 以前と以後に分かれるのでは、と思うくらいに。久しぶりに仕事で React を触ることになりそうなので、Hooks は試しておかなければ。簡単な Todo リストのサンプルを書いてみることにした。
プロジェクトは create-react-app で新規作成。create-react-app は TypeScript をサポートしているので、ちゃんとした Web アプリを React で開発するなら、もはや言語は TypeScript 一択だろう。
npx create-react-app react-todo --template typescript
Hooks は useState だけを使ってみた。App.tsx は次の通り。Todo リストを保持するグローバルなステートを App に持たせ、TodoForm にローカルなステートとして編集内容を持たせている。
import React, { useState } from 'react'; interface Todo { id?: number; name: string; completed: boolean; editing: boolean; } interface TodoFormProps { todo: Todo; onSave: (todo: Todo) => void; } const TodoForm: React.FC<TodoFormProps> = ({ todo, onSave }) => { const [name, setName] = useState(todo.name); return ( <form onSubmit={(e) => { e.preventDefault(); onSave({ ...todo, name, editing: false, }); setName(""); }}> <input type="text" value={name} onChange={(e) => setName(e.target.value)}/> <button type="submit"> Save </button> { todo.id && <button onClick={(e) => { e.preventDefault(); onSave({ ...todo, editing: false, }); }}> Cancel </button> } </form> ); }; interface TodoItemProps { todo: Todo; onEdit: (todo: Todo) => void; onDelete: (todo: Todo) => void; onComplete: (todo: Todo) => void; } const TodoItem: React.FC<TodoItemProps> = ({ todo, onComplete, onEdit, onDelete }) => ( <div> <input type="checkbox" checked={todo.completed} onChange={() => onComplete(todo)} /> { todo.completed ? <del>{todo.name}</del> : <>{todo.name}</> } <button onClick={() => onEdit(todo)}> Edit </button> <button onClick={() => onDelete(todo)}> Del </button> </div> ); interface TodoListProps { todoList: Todo[]; onEdit: (todo: Todo) => void; onDelete: (todo: Todo) => void; onUpdate: (todo: Todo) => void; onComplete: (todo: Todo) => void; } const TodoList: React.FC<TodoListProps> = ({ todoList, onEdit, onComplete, onUpdate, onDelete, }) => ( <div> {todoList.map(todo => ( todo.editing ? <TodoForm todo={todo} onSave={onUpdate}/> : <TodoItem todo={todo} onEdit={onEdit} onComplete={onComplete} onDelete={onDelete} /> ))} </div> ); const App: React.FC = () => { const [todoList, setTodoList] = useState<Todo[]>([ { id: 1, name: "foo", completed: false, editing: false }, { id: 2, name: "bar", completed: false, editing: false }, ]); const [nextId, setNextId] = useState<number>(3); return ( <div className="App"> <h1>React Todo</h1> <TodoForm todo={{name:"", completed: false, editing: false}} onSave={(todo) => { setTodoList([...todoList, { ...todo, id: nextId }]); setNextId(nextId + 1); }} /> <TodoList todoList={todoList} onDelete={(todo) => { setTodoList(todoList.filter(x => x.id !== todo.id)); }} onEdit={(todo) => { setTodoList(todoList.map(x => { if (x.id === todo.id) { return { ...x, editing: true, }; } else { return { ...x, editing: false, }; } })); }} onUpdate={(todo) => { setTodoList(todoList.map(x => { if (x.id === todo.id) { return todo; } else { return { ...x, editing: false, }; } })); }} onComplete={(todo) => { setTodoList(todoList.map(x => { if (x === todo) { return { ...x, completed: !x.completed }; } else { return x; } })); }} /> </div> ); } export default App;
yarn start
で実行して動作確認。その場編集モードもうまく動いてくれた。
余談だけど、スクリーンを録画したアニメーション GIF を作るのに ScreenToGif というツールを使ってみたら、かなり簡単に作れてすこぶる良かった。アニメーション GIF があると分かりやすさが段違いなので、積極的に活用していきたい。
今回は useState だけ試したけど、他にも useEffect や useContext や useReducer など、色んなフックがある。さらにはカスタムフックまで。「React Hooks 完全に理解した」って言えるまで、まだまだ遠いな。