Blog Article
React・TypeScript・Recoilを使ってTodoアプリを作ってみた

最近、TypeScriptやRecoilの学習をしたので、それらの技術を使ってToDoアプリを作ってみました。
ソースコード
ソースコードはGitHubに公開しているので、参考までにどうぞ!
https://github.com/kakuta0915/recoil-todo-app
Recoilとは?
Recoil (リコイル )は、Reactの状態管理ライブラリです。Reactのコンポーネント間での状態 (データ)の共有や管理を簡単にするために使われます。
- 状態の共有: 複数のコンポーネントが同じデータにアクセスできるようにし、更新があればその変更が反映されます。
- 状態の分割管理: データを「atom(アトム)」という単位で管理します。これにより、状態が大きくなりすぎず、効率的に分けて管理できます。
- 非同期の状態管理: 非同期のデータ取得や処理 (例えば、APIからデータを取ってくる) も簡単に管理できます。
Recoilは、ReactのコンテキストやReduxのように複雑なコードを書かずに、状態を効率的に管理できるため、特に大規模なアプリケーションで便利です。
環境構築
下記のコマンドを実行して、プロジェクトを作成します。TypeScriptを使いたいので、—template typescript
オプションを追加します。
npx create-react-app todo-app --template typescript
依存関係をインストールします。
npm i
ローカル環境を起動して画面が表示されていればOKです!

次は下記のリンクから、Recoilをインストールします!
https://recoiljs.org/docs/introduction/installation/
npm i recoil --save
下記は、Recoilの型定義ファイルをインストールするためのコマンドです。
TypeScriptでRecoilを使用する際、 をインストールすることで、RecoilのAPIに対する型補完や型チェックを利用できるようになります。 /recoil
npm i @types/recoil
雛形を作成したため不要なコードが記述されています。まずは、不要なコードを削除します。
App.test.tsx
は今回使わないため削除します。また、App.tsx
内の不要なコードも削除します。
// App.tsx
import React from "react";
import "./App.css";
function App() {
return <div className="App"></div>;
}
export default App;
コンポーネント作成
次にsrc
ディレクトリ配下にcomponents
ディレクトリを作成します。
components
ディレクトリ配下にInputTask
コンポーネントとAddTask
コンポーネントを作成し、index.jsx
とindex.module.css
を作成します。

InputTaskコンポーネントに以下のコードを追加します。
import React from "react";
function InputTask() {
return <div>InputTask</div>;
}
export default InputTask;
AddTaskコンポーネントに以下のコードを追加します。
import React from "react";
function AddTask() {
return <div>AddTask</div>;
}
export default AddTask;
2つのコンポーネントをAppコンポーネントにインポートして、ブラウザに表示されているか確認します。
import React from "react";
import InputTask from "./components/InputTask";
import AddTask from "./components/AddTask";
import "./App.css";
function App() {
return (
<div className="stask">
<InputTask />
<AddTask />
</div>
);
}
export default App;
CSSはとりあえず、中央配置にします。スタイルは適宜追加してください。
App.cssに以下のスタイルを記述します。
.task {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
インポートしたコンポーネントが表示されました!

要素を追加
InputTaskコンポーネント内に記述してきます。タスク追加に必要な関数などは、後に記述します。
import React from "react";
import styles from "./index.module.css";
function InputTask() {
return (
<div className={styles.inputField}>
<input type="text" className={styles.inputText} />
<button type="button" className={styles.inputButton}>
追加
</button>
</div>
);
}
export default InputTask;
AddTaskコンポーネントにも記述します。InputTaskのボタンが押下されたら、このコンポーネント内に追加される感じです。
import React from "react";
import styles from "./index.module.css";
function AddTask() {
return (
<div className={styles.taskField}>
<ul>
<li>ここにタスクが追加されます。</li>
<li>ここにタスクが追加されます。</li>
<li>ここにタスクが追加されます。</li>
</ul>
</div>
);
}
export default AddTask;
ブラウザを確認すると、こんな感じで表示されます。
ここから、Recoilを使って、タスクを追加する機能を実装していきます。

Recoilを使って、タスクリストの状態管理の設定
src
ディレクトリ配下に、states
ディレクトリを作成し、その配下にinputTask.ts
を作成し、以下のコードを記述します。
states
ディレクトリは、状態管理に関するファイルをまとめるためのディレクトリです。主にRecoilのatom
やselector
を定義するためのファイル を置く場所として使われます。status
ディレクトリを作成することで、状態管理のファイルを整理しやすくなったり、コンポーネントとは別で管理できるので再利用しやすいというメリットがあります。
import { atom } from "recoil";
export const inputTaskState = atom<string>({
key: "inputTaskSate",
default: "",
});
atom
はRecoilの状態 (データ)を管理する仕組みです。
ReactのuseState
のようなものですが、コンポーネント間で共有できるのが特徴です。
inputTaskState
は入力の状態をatom
を使って管理します。key
プロパティは、他のタスクと重複しないために、一意のkeyとして設定しています。defalut
プロパティは初期値を設定しています。
この設定をすることで、入力された内容を管理して、どこからでもアクセスできるようにしています。
Recoilの範囲を指定
次にやることは、先ほど設定したグローバルステートをどこの範囲内で使うかを設定します。
App.tsx
内にRecoilRoot
をインポートして、RecoilRoot
でラップします。こうすることで、ラップしたコンポーネント内でatomの状態を管理・共有できるようになります。
import React from "react";
import { RecoilRoot } from "recoil";
import InputTask from "./components/InputTask";
import AddTask from "./components/AddTask";
import "./App.css";
function App() {
return (
<RecoilRoot> // ← 追加
<div className="task">
<InputTask />
<AddTask />
</div>
</RecoilRoot> // ← 追加
);
}
export default App;
atomの値を取得
inputTaskState
で指定した値を取得して画面に表示してみましょう。
まず、一時的にinputTaskState
のdefault
プロパティを変えてみます。
//inputTaskState.ts
import { atom } from "recoil";
export const inputTaskState = atom<string>({
key: "inputTaskState",
default: "テスト", // ここを一時的に変更
});
InputTaskコンポーネント内に以下の記述をします。
ボタンを押下すると、inputTaskState
のデフォルト値がコンソールで表示されるようにします。
inputTaskState
の値を取得するには、RecoilのuseRecoilValue
フックを使います。
useRecoilValue
は、指定されたatom
またはselector
の値を読み取り、コンポーネントがその値を再レンダリングするときに使用します。重要なのは、useRecoilValue
は状態の読み取り専用であり、その値を更新することはできません。
import React from "react";
import { useRecoilValue } from "recoil";
import { inputTaskState } from "../../states/inputTaskState";
import styles from "./index.module.css";
function InputTask() {
// ↓ コードを追加
const inputTask = useRecoilValue(inputTaskState);
const handleClick = () => {
console.log(inputTask);
};
return (
<div className={styles.inputField}>
<input type="text" className={styles.inputText} />
<button
type="button"
className={styles.inputButton}
onClick={handleClick} // ← 追加
>
追加
</button>
</div>
);
}
export default InputTask;
問題発生!
ブラウザで確認すると、エラーが発生していました。。。

色々調べてみると、RecoilがReact19で動作しないようです。
なので、package.jsonのreactとreact-domのバージョンを^19.0.0
から^18.0.0
に変更して、再度npm install
したら、画面に表示されました。
追加ボタンを押下すると、コンソールに値が出力されて、Recoilの値を取得できていることが確認できました。

入力した値を取得
次は、入力した値をRecoilで更新して、その値を取得してみます。InputTaskコンポーネント内にコードを追加します。
useSetRecoilState
の中に入力した値を入れて、状態を更新しています。
function InputTask() {
const inputTask = useRecoilValue(inputTaskState); // 現在の値を取得
const setInputTask = useSetRecoilState(inputTaskState); // 値を更新
const handleClick = () => {
console.log(inputTask);
};
// ↓ ここを追加
const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputTask(e.target.value);
console.log(e.target.value);
},
[inputTask]
);
return (
<div className={styles.inputField}>
<input
type="text"
className={styles.inputText}
onChange={onChange} // ← 追加
value={inputTask} // ← 追加
/>
<button
type="button"
className={styles.inputButton}
onClick={handleClick}
>
追加
</button>
</div>
);
}
export default InputTask;
ボタンを押すたびに、入力した値 (inputTask
)を更新する処理です。
onChange
関数は、入力フォームの値が変わった時に呼ばれます。
<input type="text" className={styles.inputText} onChange={onChange} />
useCallback
は関数をメモ化して、無駄な再生成を防ぐために使います。
[inputTask]
という依存配列があるので、inputTask
が変わったときだけonChange
を作り直しています。
const onChange = useCallback((e) => { ... }, [inputTask]);
この部分は、入力された値をリアルタイムで更新&表示する処理をしています。
e
はイベントオブジェクトReact.ChangeEvent<HTMLInputElement>
は「これはinput
のonChange
で発生するイベントだよ」と型を指定しているe.target.value
をsetInputTask
に渡して、状態(state )を更新する
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputTask(e.target.value);
console.log(e.target.value);
}
フォームに文字を入力すると、入力するたびにコンソールに出力されました。

値の取得ができたので、タスクリストの値の型を指定する設定をします。src
ディレクトリ配下にtypes
ディレクトリを作成し、Task.ts
ファイルを作成し、以下のコードを記述します。
これは、タスクリストのデータ構造を定義しています。
export type Task = {
id: string;
title: string;
};
次に、タスクリストの状態を管理するための記述をします。states
ディレクトリ配下に、addTaskState.ts
ファイルを作成し、以下のコードを記述します。
import { atom } from "recoil";
import { Task } from "../types/Task";
export const addTaskState = atom<Array<Task>>({
key: "addTaskState",
default: [],
});
atom<Array<Task>>
は、先ほど定義したTask型のオブジェクトの配列 (Array)を管理するための状態であることを意味しています。
タスクを追加する
次は追加ボタンを押下したら、タスクを追加する機能を実装してみたいと思います。
InputTaskコンポーネント内のhandleClick
関数に処理を書いていきます。
import React, { useCallback } from "react";
import { useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
import { inputTaskState } from "../../status/inputTaskState";
import styles from "./index.module.css";
import { addTaskState } from "../../status/addTaskState";
const getKey = () => Math.random().toString(36).substring(2); // 追加
function InputTask() {
const inputTask = useRecoilValue(inputTaskState);
const setInputTask = useSetRecoilState(inputTaskState);
const [addTask, setAddTask] = useRecoilState(addTaskState); // 追加
// 追加
const handleClick = () => {
setAddTask([...addTask, { id: getKey(), title: inputTask }]);
setInputTask("");
};
const onChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setInputTask(e.target.value);
console.log(inputTask);
},
[inputTask]
);
const getKey = () => Math.random().toString(36).substring(2);
Math.random()
はランダムな数を生成します。それを文字列に変換して、さらに一部を切り出すことでユニークなIDを作成できます。
const handleClick = () => {
setAddTask([...addTask, { id: getKey(), title: inputTask }]);
setInputTask("");
console.log(addTask);
};
handleClick
関数は、タスクを追加する役割を持っています。この関数は、クリックイベントが発生したときに実行されます。
setAddTask
は、Recoilの状態を更新するための関数です。addTask
という状態を更新する際に使われます。addTask
は現在のタスクのリストです。このリストに新しいタスクを追加します。第一引数にスプレッド構文で、
addTask
配列の中身を展開して新しい配列を作成します。第二引数に追加したい配列を指定します。- 新しいタスクの情報は
{ id: getKey(), title: inputTask }
で指定されています。getKey()
は一意なIDを生成する関数で、inputTask
はユーザーが入力したタスクのタイトルです。
これによって、新しいタスクが addTask
の配列に追加されます。
次は、addTaskコンポーネント内にタスクを追加する機能を実装します。
import React from "react";
import { useRecoilValue } from "recoil";
import { addTaskState } from "../../status/addTaskState";
import styles from "./index.module.css";
function AddTask() {
const addTask = useRecoilValue(addTaskState);
return (
<div className={styles.taskField}>
<ul className={styles.taskList}>
{addTask.map((task) => {
return <li key={task.id}>{task.title}</li>;
})}
</ul>
</div>
);
}
export default AddTask;
addTask
で配列を取得しています。
あとは、addTask
の配列をmap関数で展開して表示しています。
追加ボタンを押下するとタスクが下に追加されました。

最後に
今回は、ToDoアプリケーションを作成しながら、TypeScriptの基本構文やRecoilを使った状態管理方法について理解を深めることができました。
Recoilは、React19だと動作しないので、もし動作しないときはバージョンを確認しましょう!また、Recoilは現在開発停止しているようで、Jotaiというライブラリに移行したほうが良いと言われています。JotaiはRecoilと設計が近くとっつきやすいみたいなので、余裕があれば勉強したいと思います!
コメント
ログインしてコメントしましょう。