Logo Image

Blog Article

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

00

最近、TypeScriptやRecoilの学習をしたので、それらの技術を使ってToDoアプリを作ってみました。

ソースコード

ソースコードはGitHubに公開しているので、参考までにどうぞ!

https://github.com/kakuta0915/recoil-todo-app

Recoilとは?

Recoil (リコイル )は、Reactの状態管理ライブラリです。Reactのコンポーネント間での状態 (データ)の共有や管理を簡単にするために使われます。

  1. 状態の共有: 複数のコンポーネントが同じデータにアクセスできるようにし、更新があればその変更が反映されます。
  2. 状態の分割管理: データを「atom(アトム)」という単位で管理します。これにより、状態が大きくなりすぎず、効率的に分けて管理できます。
  3. 非同期の状態管理: 非同期のデータ取得や処理 (例えば、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を使用する際、@types/recoil をインストールすることで、RecoilのAPIに対する型補完や型チェックを利用できるようになります。

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.jsxindex.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のatomselectorを定義するためのファイル を置く場所として使われます。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で指定した値を取得して画面に表示してみましょう。

まず、一時的にinputTaskStatedefaultプロパティを変えてみます。

//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>は「これはinputonChangeで発生するイベントだよ」と型を指定している
  • e.target.valuesetInputTaskに渡して、状態(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と設計が近くとっつきやすいみたいなので、余裕があれば勉強したいと思います!

コメント

ログインしてコメントしましょう。