React+Recoil+TypeScriptでTODOアプリの作成
Recoilが新しくでたので、使ってみました。
まず、フォルダ構成は以下の通り
Recoilのフォルダ構成は上記のようにatomのフォルダを作ってその中に書いていた記事が多かったので、それと同じようにしました。
create-react-appを使ってTypeScriptを導入する場合は以下のコマンドを打ちます。
npx create-react-app フォルダ名 --typescript
Recoilのインストールは以下のコマンドを打ちます。
npm install recoil
とりあえず、環境構築はこれで終わりです。
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
ここは何も変えていません。
App.tsx
import React from 'react'; import './App.css'; import { RecoilRoot } from 'recoil'; import TextForm from './components/TextForm'; import TaskList from './components/TaskList'; const App: React.FC = () => { return ( <RecoilRoot> <div> <TextForm /> <TaskList /> </div> </RecoilRoot> ) } export default App;
TextFormでTodoを追加する所を作っていきます。
TaskListでTodoの一覧を作ります。
atom.ts
import { atom } from 'recoil'; export type Item = { id: number title: string, completed: boolean; } const initState: Item[] = []; export const taskState = atom({ key: "task", default: initState });
今回はInterfaceではなくすべてType aliasで書いています。
一応違いはあるようですが、TypeScriptを勉強し始めて2日なので、今は気にしないことにします。
TextForm.tsx
import React, { useState } from "react"; import { useSetRecoilState } from "recoil"; import { taskState } from '../atoms/atom'; const TextForm: React.FC = () => { const [title, setTitle] = useState<string>(""); const [number, setNumber] = useState<number>(0); const setTasks = useSetRecoilState(taskState); const onChange = (e: React.ChangeEvent<HTMLInputElement>) => { setTitle(e.target.value) } const onAdd = () => { setNumber(number+1); setTasks(t => [...t, { id: number, title, completed: false }]) //titleはtitle:tilteと同じになるので省略できる setTitle("") } return ( <div> <input type="text" value={title} onChange={onChange}></input> <button onClick={onAdd}>登録</button> </div> ) } export default TextForm;
onChangeのe: React.ChangeEvent
ここではTodoを追加するだけなので書き込み専用である、useSetRecoilStateを使っています。
ここも特に難しいことはやっていないです。
TaskList.tsx
import React from 'react'; import { useRecoilState } from "recoil"; import { taskState, Item } from '../atoms/atom'; const TaskList: React.FC = () => { const [tasks, setTasks] = useRecoilState(taskState); const isCompleted = (task: Item) => { const copytasks = [...tasks]; const copytask = { ...task }; copytask.completed = !copytask.completed; const index=copytasks.indexOf(task); copytasks.splice(index, 1, copytask); setTasks(copytasks); } const onRemove = (id: number) => { const copytasks = [...tasks]; const index=copytasks.findIndex(int=>int.id===id); copytasks.splice(index, 1); setTasks(copytasks); } return ( <div> <ul> {tasks.map(task => ( <li key={task.id}> <input type="checkbox" checked={task.completed} onChange={() => isCompleted(task)}></input> <span style={{ textDecoration: task.completed ? "line-through" : "" }}> {task.title} </span> <button onClick={() => onRemove(task.id)}>削除</button> </li> ))} </ul> </div> ) } export default TaskList;
まず、isCompletedの処理から説明していきます。
copytasksとcopytaskはtasksが読み取り専用なので、一旦コピーしているだけです。
const indexでは.indexOfを使ってtaskが何番目にあるのかを探します。存在しない場合は-1を返します。
copytasks.spliceで変更があったtaskを書き換えます。
あとは、 setTasksでTodoの上書きをして終わりです。
onRemoveでは、引数にとったidを元にfindIndexを使ってそのtaskが何番目にあるのかを特定し、spliceを使ってその要素を取り除くだけです。
感想
RecoilとReduxどちらも使ってみた感想は、Recoilは後発というだけあって、hooksのように使えReduxよりも圧倒的に記述量が少く使いやすいです。
私的には、Recoil推しです。