RecoilはReactライブラリで利用することができる状態管理ライブラリの中の1つです。Reactやフレームワークを学習し始めた人は状態管理というのがどういうものかイメージしにくいかもしれませんが要は複数の場所(コンポーネント)からアクセスできる場所を作成しその場所に共有する状態(データ)を保存して管理することです。Recoilを含め状態管理ライブラリを導入するとアプリケーションを構成する複数のコンポーネントから共有した状態(データ)の取得、更新を行えるようになります。

ReactにはReduxをはじめさまざまな状態管理ライブラリが提供されていますがRecoilはReactはReduxにと比較すると初期設定がシンプルでわかりやすくなっています。

本文書ではRecoilの公式ドキュメントにあるBasic Tutorialのコード(Todoアプリ)を参考にしながらRecoilの使う上で必須となる機能を確認していきます。本文書を読み終えるとRecoilで共有する状態(データ)を定義するAtom、共有した状態(データ)を操作(Atomの値を利用して別の処理を行う)するSelector、共有した状態(データ)の読み込み/書き込みを行うためのHooksの利用方法を理解することができます。

React環境の構築

Recoilの動作確認を行うReactのプロジェクトを作成します。react-recoilという名前のプロジェクトを作成しています。プロジェクト名は任意なので好きな名前をつけてください。


 % npx create-react-app react-recoil

Recoilの初期設定

Reactプロジェクトの作成完了後、npmコマンドでrecoilライブラリのインストールを行います。


 % nnpm install recoil

最初にRecoilで状態管理を行いたいコンポーネントをRecoilコンポーネントで囲む必要があります。本文書ではTodoListコンポーネントを作成しそのコンポーネント下Recoilを利用して状態管理を行いたいのでApp.jsファイルでTodoListコンポーネントをimportしてRecoilコンポーネントで囲みます。


import { RecoilRoot } from 'recoil';
import TodoList from './components/TodoList';

function App() {
  return (
    <div style={{ margin: '2em' }}>
      <RecoilRoot>
        <TodoList />
      </RecoilRoot>
    </div>
  );
}

export default App;

srcフォルダにcomponentsフォルダを作成してTodoList.jsファイルを作成します。


function TodoList() {
  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
    </>
  );
}

export default TodoList;

npm startコマンドで開発サーバを起動します。


 % npm start
Compiled successfully!

You can now view react-recoil-basic in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.2.118:3000

ブラウザでhttp://localhost:3000にアクセスすると下記の画面が表示されます。

Reactの動作確認
Reactの動作確認

ここまででRecoilを設定するための環境の構築は完了です。ここからRecoilを利用してTodoアプリを作成していきます。

Todoアプリの作成

Atomの設定

Recoilではatomを利用して共有したい状態の定義を行います。Recoilを利用する上でatomでの定義は必須です。atomでは初期値を設定し必ずアプリケーション内のatom(と後ほど説明するselector)の中で一意となるkeyを設定する必要があります。またatomでは初期値を設定する必要があります。atomは複数設定することができ個々のatomは独立した状態を持ちそれぞれを複数のコンポーネントで共有することができます。

atomを利用してTodoのリストを定義します。keyプロパティに一意となる名前、defaultプロパティに配列で初期値を設定してtodoListStateという名前の変数に保存します。


import { atom } from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: [
    {
      id: 0,
      title: 'メール送信',
      isComplete: false,
    },
  ],
});

//略

atomを利用して共有することができる状態を作成したので次はその状態にアクセスする方法を確認します。

共有した状態へのアクセス方法

atomで定義した状態へは直接アクセスするのではなくuseRecoilValue Hookを含めHooksを利用する必要があります。

RecoilからimportしたuseRecoilValueの引数にはtodoListStateを設定します。設定するtodoListStateはkeyではなくatomの戻り値を保存した変数です。useRecoilValueから戻される値をtodoListに保存しatomに設定した状態の値がtodoListに入っているかconsole.logを利用して確認します。


import { atom, useRecoilValue } from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: [
    {
      id: 0,
      title: 'メール送信',
      isComplete: false,
    },
  ],
});

function TodoList() {
  const todoList = useRecoilValue(todoListState);
  console.log(todoList);
  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
    </>
  );
}

export default TodoList;

ブラウザのコンソールを確認するとatomで設定した初期値の配列を確認することができます。

Atomで設定した状態を取得
Atomで設定した状態を取得

useRecoilValue Hookを利用することでatomの値を取得することが確認できました。useRecoilValue HookはRead Onlyなので状態の取得はできますが更新を行うことはできません。

Recoilではatomの値にアクセスするために用意されたHookが他にもありuseRecoilState Hookを利用するとReactのuseState Hookと同じように値とsetter関数が戻されます。


 const [todoList, setTodoList] = useRecoilState(todoListState);

useRecoilState Hookを利用して戻されたsetTodoListを利用してatomの値を更新することができます。更新方法は後ほど確認します。todoListにはuseRecoilValue Hookと同様にatomで設定した初期値が設定されます。setter関数だけを利用したい場合にはuseSetRecoilState Hookを利用することもできます。

ここまでの説明で3つのHooksが登場しました。値のみ取得したい場合はuseRecoilValue, setter関数のみ利用したい場合はuseSetRecoilState、両方利用したい場合はuseRecoilStateとなり違いはシンプルです。

useRecoilValueで取得したtodoListをmap関数で展開してブラウザ上に表示させます。


//略
function TodoList() {
  const todoList = useRecoilValue(todoListState);
  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      {todoList.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

atomに設定した初期値をブラウザ上に表示することができました。

map関数で展開してTodoリストを表示
map関数で展開してTodoリストを表示

selectorの利用

Recoilの中でatomと同様に重要な機能の一つであるselectorの利用方法について確認を行っていきます。atomとは異なりselectorは必須ではありません。selectorはatomの状態を操作したい場合(atomの値を利用して別の処理を行う)に利用することができます。ここで行うselectorの動作確認ではatomに設定した配列の中のオブジェクトの数を確認することでTodoのリストにTodoが何個登録されているのか取得します。

selectorはatomと同様にkeyプロパティで一意の名前をつける必要がありますがatomとは異なり初期値の設定は行いません。defaultの代わりにgetプロパティとsetプロパティを設定することができます。getプロパティのget関数はselectorの中のみで利用することができatomやselectorにアクセスすることができます。setプロパティを利用してatomの値を更新することもできます。get関数の引数にatomの戻り値を保存したtodoListStateを指定してTodoリストを首藤してtodoListに保存しています。その後todoListは配列なのでlengthで配列の大きさを取得して戻しています。selectorの戻り値はtodoListStatsState変数に保存しています。


const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    return totalNum;
  },
});

設定したselectorを利用したい場合はatomと同様にuseRecoilValue Hookを利用することができます。useRecoilValue Hookの引数にselectorの戻り値を持つ変数todoListStatsStateを設定します。


const totalNum = useRecoilValue(todoListStatsState);

selectorを利用して定義したtotalNumを利用してTodoのリストの数をブラウザ上に表示させます。Todoリストが追加や削除が行われた場合はselectorは再計算を行いブラウザ上の値に反映されます。


import { atom, selector, useRecoilValue } from 'recoil';

const todoListState = atom({
  key: 'todoListState',
  default: [
    {
      id: 0,
      title: 'メール送信',
      isComplete: false,
    },
  ],
});

const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    return totalNum;
  },
});

function TodoList() {
  const todoList = useRecoilValue(todoListState);
  const totalNum = useRecoilValue(todoListStatsState);
  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <ul>
        <li>Todoの登録数:{totalNum}</li>
      </ul>
      {todoList.map((item) => (
        <div> key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

export default TodoList;

selectorで設定した関数を利用してブラウザ上にTodoListの数を表示させることができました。

selectorで取得した値を表示
selectorで取得した値を表示

ファイルの分割

ここまで確認した通り1つのファイルの中にatom, selectorやatom, selectorを利用する処理を記述することができますがコードが増えてくると管理するのが大変になるのでファイルを分割します。Recoilのフォルダ構成のベストプラクティスを見つけることができませんでしたが本文書ではsrcフォルダにatom.jsファイルとselector.jsファイルを作成してそれぞれのファイルのatomとselectorの設定を記述します。atomの数が増えればこの方法でも管理が難しくなると思うので別のフォルダ構成を考える必要があります。

atom.jsファイルにはatomの設定のみ記述します。


import { atom } from 'recoil';

export const todoListState = atom({
  key: 'todoListState',
  default: [
    {
      id: 0,
      title: 'メール送信',
      isComplete: false,
    },
  ],
});

selector.jsファイルにはselectorの設定のみ記述します。selectorではatomを利用するためにatome.jsファイルで定義したtodoListStateをimportして利用しています。


import { selector } from 'recoil';
import { todoListState } from './atom';

export const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    return totalNum;
  },
});

TodoList.jsファイルではatomとselectorの設定を別ファイルに移動したのでatomとselectorはimportして利用する必要があります。


import { useRecoilValue } from 'recoil';
import { todoListState } from '../atom';
import { todoListStatsState } from '../selector';

function TodoList() {
  const todoList = useRecoilValue(todoListState);
  const totalNum = useRecoilValue(todoListStatsState);
  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <ul>
        <li>Todoの登録数:{totalNum}</li>
      </ul>
      {todoList.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

export default TodoList;

他のコンポーネントからのアクセス

ここまではRecoilを利用してatomとselectorを定義しましたがTodoListコンポーネントのみからアクセスを行っているため他のコンポーネントからもアクセスできるか確認していません。

他のコンポーネントからもアクセスができることを確認するためにcomponentsフォルダにTodoListStats.jsファイルを作成します。TodoListコンポーネントからTodoListStatsコンポーネントにselectorの処理とその描写機能を移動します。


import { useRecoilValue } from 'recoil';
import { todoListStatsState } from '../selector';

function TodoListStats() {
  const totalNum = useRecoilValue(todoListStatsState);
  return (
    <ul>
      <li>Todoの登録数:{totalNum}</li>
    </ul>
  );
}

export default TodoListStats;

作成したTodoListStatsコンポーネントはTodoListコンポーネントでimportします。


import { useRecoilValue } from 'recoil';
import { todoListState } from '../atom';
import TodoListStats from './TodoListStats';

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      {todoList.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

export default TodoList;

TodoListStatsコンポーネントからselectorにアクセスできるためブラウザに表示される内容はそのままです。

selectorで取得した値を表示
selectorで取得した値を表示

共有した状態を更新する

共有した状態へのアクセス方法は理解できたので次は更新方法について確認を行っていきます。Todoリストに新たにTodoをリストを追加するコードを作成していきます。

Todoを追加するためのinput要素を追加します。useState Hookを利用してinput要素で入力した値を保存します。AddボタンをクリックするとaddItem関数が実行されinput要素に入力した値がコンソールに表示されます。


import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { todoListState } from '../atom';
import TodoListStats from './TodoListStats';

function TodoList() {
  const [title, setTitle] = useState('');
  const todoList = useRecoilValue(todoListState);

  const handleChange = (e) => {
    setTitle(e.target.value);
  };

  const addItem = () => {
    console.log(item);
    setTitle['']
  };

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      <div style={{ margin: '1em 0' }}>
        <input type="text" value={title} onChange={handleChange} />
        <button onClick={addItem}>Add</button>
      </div>
      {todoList.map((item) => (
        <div key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

export default TodoList;

ここまで追加は通常のReactで慣れたコードだと思います。次はaddItem関数の中で共有した状態の更新を行います。atomで定義した初期値の配列にinput要素で入力したTodoを追加します。

todoListの取得だけではなく更新処理を行うためにuseRecoilValueからuseRecoilStateに変更します。


import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from '../atom';
import TodoListStats from './TodoListStats';

function TodoList() {
  const [title, setTitle] = useState('');
  const [todoList, setTodoList] = useRecoilState(todoListState);
  //略

useRecoilState HookによりsetTodoList関数が利用できるようになったのでaddItemの中で配列への追加処理を行います。配列への追加へはSpread Operatorを利用しています。


const addItem = () => {
  setTodoList((oldTodoList) => [
    ...oldTodoList,
    {
      id: getId(),
      title: title,
      isComplete: false,
    },
  ]);
  setTitle('');
};

getId関数についてはTodlListの中で下記のように定義しておきます。map関数でtodoListを展開する際にkey属性を設定する必要がありkey属性は一意にするためgetId関数を利用しています。


let id = 1;
function getId() {
  return id++;
}

input要素に文字列を追加してAddボタンを押すとTodoリストに追加が行われ、selectorで設定しているTodoListの数も2に増えることが確認できました。

リストへの追加
リストへの追加

共有した状態へのアクセスに続き、共有した状態への更新を行うことができるようになりました。

Input要素を含む描写部分のコードとTodoの更新処理をTodoItemCreator.jsファイルを作成してその中に移動します。


import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { todoListState } from '../atom';

function TodoItemCreator() {
  const [title, setTitle] = useState('');
  const setTodoList = useSetRecoilState(todoListState);

  const handleChange = (e) => {
    setTitle(e.target.value);
  };

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        title: title,
        isComplete: false,
      },
    ]);
    setTitle('');
  };

  return (
    <div style={{ margin: '1em 0' }}>
      <input type="text" value={title} onChange={handleChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

export default TodoItemCreator;

let id = 1;
function getId() {
  return id++;
}

setTodoList関数を取得するためにuseRecoilState Hookを利用していましたがTodoItemCreator.jsではtodoListを利用して描写処理は行わないため必要ありません。そのためuseSetRecoilState Hookを利用してsetter関数のsetTodoListのみ使っています。

TodoListコンポーネントからIput要素と処理をTodoItemCreatorコンポーネントに移動したのでTodoListではTodoItemCreatorコンポーネントをimportします。

TodoListコンポーネントではsetTodoList関数が必要なくなったのでuseRecoilState HookからuseRecoilValue Hookに戻してtodoListのみ取得しています。


import { useRecoilValue } from 'recoil';
import { todoListState } from '../atom';
import TodoListStats from './TodoListStats';
import TodoItemCreator from './TodoItemCreator';

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      <TodoItemCreator />
      {todoList.map((item) => (
        <div> key={item.id}>{item.title}</div>
      ))}
    </>
  );
}

export default TodoList;

変更後動作に違いはありません。

リストへの追加
リストへの追加TodoItemリストへの追加

ここまでの動作確認でRecoilの主要な機能であるAtom, Selector, Hooks(useRecoilValue, useRecoilState, useSetRecoilState)の使い方を理解することができます。残りのTodoアプリはそれらの機能を利用してさらに機能を追加していくだけです。

Itemコンポーネントの作成

componentsフォルダにTodoItem.jsファイルを作成してmap関数で展開したitemをTodoItemコンポーネントで描写します。TodoItemではTodoListコンポーネントからpropsでitemを受け取ります。


function TotoItem({ item }) {
  return <div>{item.title}</div>;
}

export default TotoItem;

TodoListコンポーネントでTodoItemをimportして下記のように記述します。


import { useRecoilValue } from 'recoil';
import { todoListState } from '../atom';
import TodoListStats from './TodoListStats';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';

function TodoList() {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      <TodoItemCreator />
      {todoList.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </>
  );
}

export default TodoList;

ここからはTodoItemの中で削除機能などを追加していきます。

Todoの削除

Todoリストへの追加機能を実装は完了したので今度は削除機能の実装を行います。 Todoのタイトルの横に”X”を追加しクリックイベントにdeleteItem関数を設定します。


function TotoItem({ item }) {
  const deleteItem = () => {
    console.log('delete');
  };

  return (
    <div>
      {item.title}
      <span onClick={deleteItem} style={{ cursor: 'pointer' }}>
        X
      </span>
    </div>
  );
}

export default TotoItem;

Todoの横に”X”が表示されクリックするとコンソールに”delete”が表示されます。

削除ボタンの追加とクリックイベント設定
削除ボタンの追加とクリックイベント設定

TodoListの情報を削除するためTodoListとsetTodoListが必要になるのでuseRecoilState HookとatomからtodoListStateをimportします。


import { useRecoilState } from 'recoil';
import { todoListState } from '../atom';
function TotoItem({ item }) {
  const [todoList, setTodoList] = useRecoilState(todoListState);
//略

todoListとsetTodoListを利用してdeleteItem関数の中身を設定します。

findIndex関数を利用してTodoItemコンポーネントに渡されたitemのtodoListの配列での配列番号を取得しています。newTodoListではSpread Operatorとslice関数を利用して0から配列の番号までの要素と配列の番号+1から最後の要素までを取得して2つを繋げ配列番号indexを持つ要素のみ配列に含めないようにしています。元の配列から配列番号を持つ要素のみ削除されることになります。


const deleteItem = () => {
  const index = todoList.findIndex((listItem) => listItem.id === item.id);
  const newTodoList = [
    ...todoList.slice(0, index),
    ...todoList.slice(index + 1),
  ];
  setTodoList(newTodoList);
};

input要素を利用してTodoを2つ追加します。

2つのTodoを追加
2つのTodoを追加

真ん中にデスクの掃除の”X”をクリックしてください。デスクの掃除がTodoリストから削除されることが確認できます。デスクの削除と同時にTodoの登録数も3から2へ減少するのが確認できます。

Todoの削除
Todoの削除

完了・未完了の切り替え

Todoの中にはidとtitle以外にTodoが完了したかどうかを表すisCompleteプロパティがあります。Todoを追加直後はfalseになっているためTodoが完了したら完了であるtrueに変更できるように処理の追加を行います。

TodoItemコンポーネントにボタン要素を追加してクリックイベントにtoggleItemCompletion関数を設定します。条件 (三項) 演算子を利用してTodoのisCompleteがtrueであれば完了の”完”を表示させfalseであれば未完了の”未”を表示させます。


<div>
  <button onClick={toggleItemCompletion}>
    {item.isComplete ? '完' : '未'}
  </button>
  {item.title}
  <span onClick={deleteItem} style={{ cursor: 'pointer' }}>
    X
  </span>
</div>

クリックイベントに設定したtoggleItemCompletion関数を設定します。処理の内容はdeleteItem関数とほとんど同じでtoggleItemCompletion関数ではボタンをクリックしたitemのisCompleteプロパティの値をfalseの場合はtrueにtrueの場合はfalseになるように設定を行っています。


const toggleItemCompletion = () => {
  const index = todoList.findIndex((listItem) => listItem.id === item.id);
  const newTodoList = [
    ...todoList.slice(0, index),
    { ...item, isComplete: !item.isComplete },
    ...todoList.slice(index + 1),
  ];
  setTodoList(newTodoList);
};

ブラウザを確認するとTodoのタイトルの横にボタンが追加されデフォルトでは”未”が表示されています。

完了・未完了の切り替えボタンの表示
完了・未完了の切り替えボタンの表示

PCの起動の”未”ボタンをクリックしてください。ボタンが”未”から”完”に切り替わります。

未から完への切り替え
未から完への切り替え

再度”完”ボタンをクリックすると”未”に切り替えることができます。

未完了数、完了数の表示

追加したTodoが完了した場合に完了状態に変更することができるようになりました。Todoの登録数だけではなくTodoの未完了の数、完了の数がわかるようにselectorの更新を行います。


import { selector } from 'recoil';
import { todoListState } from './atom';

export const todoListStatsState = selector({
  key: 'todoListStatsState',
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
    const totalUncompletedNum = totalNum - totalCompletedNum;
    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
    };
  },
});

selector.jsファイルのgetプロパティにtotalCompletedNum, totalCompletedNum関数を追加します。totalCompletedNum関数ではtodoListにfilter関数を利用してisCompleteプロパティがtrueの完了済みのitemのみ取得してlengthで配列の大きさを計算しています。totalCompletedNumはtotalNumからtotalCompletedNumを引いて未完了のitemの数を取得しています。

totalNumという1つの関数のexportから複数の関数をexportすることになるので戻り値には3つの関数を含むオブジェクトに変更しています。

設定したselectorはTodoListStatsコンポーネントで利用します。


import { useRecoilValue } from 'recoil';
import { todoListStatsState } from '../selector';

function TodoListStats() {
  const { totalNum, totalCompletedNum, totalUnCompleteNum } =
    useRecoilValue(todoListStatsState);
  return (
    <ul>
      <li>Todoの登録数:{totalNum}</li>
      <li>完了の数:{totalCompletedNum}</li>
      <li>未完了の数:{totalUnCompleteNum}</li>
    </ul>
  );
}

export default TodoListStats;

ブラウザを開くとデフォルトの状態では未完了の数が1と表示されます。

完了数と未完了数の表示
完了数と未完了数の表示

Todoを2つ追加後にPCの起動を完了にすると完了の数が1となり、未完了の数が2、全体の登録数が3で正しく動作することが確認できます。selectorを利用することで完了、未完了の数を表示する機能を追加することができました。

完了の数の確認
完了の数の確認

リストのフィルター機能設定

ここまでの動作確認では登録したすべてのTodoがブラウザ上に表示されていましたが完了のTodo, 未完了のTodo, すべてのTodoの表示内容を切り替えれるように新たにatomとselectorの定義を行います。

表示を変更するために利用するselect要素をTodoListコンポーネントに追加します。


<>
  <h1>RecoilによるTodoアプリ</h1>
  <TodoListStats />
  <select>
    <option value="すべて">すべて</option>
    <option value="完了">完了</option>
    <option value="未完了">未完了</option>
  </select>
  <TodoItemCreator />
  {todoList.map((item) => (
    <TodoItem key={item.id} item={item} />
  ))}
</>

select要素が表示されオプションで設定した”すべて”、”完了”、”未完了”を選択することができるようになります。selectで変更を行っても何も処理を追加していなのでTodoリストに関連するブラウザ上の変化はありません。

selectによりオプションを選択
selectによりオプションを選択

選択した値(状態)がコンポーネント間で共有できるようにtodo.jsファイルにatomを追加します。


export const todoListFilterState = atom({
  key: 'todoListFilterState',
  default: 'すべて',
});

atomではkeyとdefaultを設定する必要があるのでkeyをtodoListFilterState、デフォルト値をselect要素のオプションで設定した”すべて”とします。

定義したatomを利用するためにTodoListコンポーネントではuseRecoilState Hookを利用します。useRecoilStateの引数に設定するtodoListFilterStateはatom.jsファイルからimportします。


import { useRecoilValue,useRecoilState } from 'recoil';
import { todoListState,todoListFilterState } from '../atom';
import TodoListStats from './TodoListStats';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';

function TodoList() {
  const todoList = useRecoilValue(todoListState);
  const [filter, setFilter] = useRecoilState(todoListFilterState);
  //略

filterとsetFilterを利用してselectで設定した値を保存します。select要素にクリックイベントを設定してhandleChange関数を設定します。handleChange関数ではsetFilter関数を利用してselectで選択した値を保存します。


function TodoList() {
  const todoList = useRecoilValue(todoListState);
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const handleChange = (e) => {
    setFilter(e.target.value);
  };

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      <select value={filter} onChange={handleChange}>
        <option> value="すべて">すべて</option>
        <option> value="完了">完了</option>
        <option> value="未完了">未完了</option>
      </select>
      <TodoItemCreator />
      {todoList.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </>
  );
}

filterの値を利用して表示するリストを変更できるようにselectorを追加します。selectorではatomで共有した状態のtodoListとfilterを利用します。switch関数を利用することでfilterの値によって戻るTodoリストを変えています。表示するTodoリストはfilter関数とTodoのisCompleteプロパティの値を利用しています。


import { selector } from 'recoil';
import { todoListFilterState, todoListState } from './atom';
//略
export const filteredTodoListState = selector({
  key: 'filteredTodoListState',
  get: ({ get }) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case '完了':
        return list.filter((item) => item.isComplete);
      case '未完了':
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

TodoList.jsではTodoリストの表示にatomのtodoListStateを利用していましたがtodoListStateからselectorのfilteredTodoListStateに変更します。


import { useRecoilValue, useRecoilState } from 'recoil';
import { todoListFilterState } from '../atom';
import TodoListStats from './TodoListStats';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';
import { filteredTodoListState } from '../selector';

function TodoList() {
  const todoList = useRecoilValue(filteredTodoListState);
  const [filter, setFilter] = useRecoilState(todoListFilterState);
//略

これでリスト表示の設定は完了でselect要素で”すべて”から”完了”に変更すると完了のTodoリストのみ表示されます。

完了のTodoリストのみ表示
完了のTodoリストのみ表示

未完了のTodoリストのみ表示することもできます。

未完了のTodoリストのみ表示
未完了のTodoリストのみ表示

TodoListコンポーネントからフィルターに関連する部分をTodoListFilter.jsファイルに移動します。


import { useRecoilState } from 'recoil';
import { todoListFilterState } from '../atom';

function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const handleChange = (e) => {
    setFilter(e.target.value);
  };
  return (
    <>
      <select value={filter} onChange={handleChange}>
        <option> value="すべて">すべて</option>
        <option> value="完了">完了</option>
        <option> value="未完了">未完了</option>
      </select>
    </>
  );
}

export default TodoListFilters;

TodoListコンポーネントは作成したTodoListFilterコンポーネントをimportして利用します。


import { useRecoilValue } from 'recoil';
import TodoListStats from './TodoListStats';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';
import { filteredTodoListState } from '../selector';
import TodoListFilters from './TodoListFilters';

function TodoList() {
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <h1>RecoilによるTodoアプリ</h1>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />
      {todoList.map((item) => (
        <TodoItem key={item.id} item={item} />
      ))}
    </>
  );
}

export default TodoList;

Todoアプリを作成することができました。

Todoアプリの作成を通してRecoilのAtom, Selectorの設定、useRecoilValue, useRecoilState, useSetRecoilState Hookの違いと使い分けも理解できるようになったかと思います。