• 最終更新日:

【React】状態管理をRecoilを使って行うチュートリアル

【React】状態管理をRecoilを使って行うチュートリアル

今回はReactの状態管理をRecoilを使って行う方法について説明します。Recoilはまだ実験段階のライブラリですが、Zennなどいくつかのサービスで利用されはじめています。

前提条件は以下

  • VSCode上でcreat react appを使ってReact、TypeScriptの環境で行う
  • ESLint(Airbnb)とPrettierを追加して、VSCode上の拡張機能と連動させる

環境は以下です。

  • macOS Catalina v10.15.5
  • Visual Studio Code v1.57.0
  • React v17.0.2
  • Recoil v0.6.1
  • TypeScript v4.5.5
  • node.js v16.13.1
この記事の目次

Recoilを説明するためのデモ

今回のチュートリアルでは以下のアプリを作りながら説明します。

input部分にテキストを入力して「登録」ボタンをクリックすると、テキストが表示されていきます。表示されたテキストの数は「○個のタスクがあります」部分に反映されます。

シンプルな機能なのでRecoilの使い方もわかりやすいと思います。

VSCodeで開発環境を作る

開発環境は以下の記事に沿って構築しています。
この開発環境に追加する形でRecoilをインストールしていきます。

Recoilのインストール

Recoil本体とTypeScriptで使う型をインストールします。

npm i --save recoil @types/recoil

Recoilの詳細については以下をどうぞ。

ディレクトリの構成について

説明するディレクトリ構成は以下です。
状態の管理はcomponentsの中にあるstatesディレクトリで行います。

|_public/
|  |- index.html
|
|_src/
   |- assets/ (共通で使うCSSや画像をまとめている)
   |   |- css/
   |   |   |- _base.scss
   |   |   |- _custom-property.scss
   |   |   |- styles.scss
   |   |
   |   |- img/
   |
   |- components/(コンポーネントをまとめている)
   |   |- blocks/(ページを構成するコンポーネント)
   |   |   |- AddBlock.module.scss
   |   |   |- AddBlock.tsx
   |   |   |- InputBlock.module.scss
   |   |   |- InputBlock.tsx
   |   |
   |   |- pages/(全ページを管理する)
   |   |   |- home/(トップページ)
   |   |   |   |- Page.module.scss
   |   |   |   |- Page.tsx
   |   |
   |   |- types/(共通で使う型をまとめている)
   |   |   |- items.ts
   |   |
   |   |- states/(状態を管理する)
   |       |- addTextState.ts
   |       |- inputTextState.ts
   |
   |- App.tsx
   |- index.tsx

typesディレクトリにあるitems.tsにはアプリケーション全体で使用する型を書きます。今回は以下だけです。

export type Items = {
  id: string;
  text: string;
};

input要素に入力したテキストの状態を管理する

input要素に入力したテキストとは、以下に書いた部分になります。

入力したテキストの状態管理をRecoilに書くと以下です。
statesディレクトリのinputTextState.tsに書いています。

import { atom } from 'recoil';

export const inputTextState = atom<string>({
  key: 'inputTextState',
  default: '',
});

Recoilでは状態管理をする場合、atomを宣言して使用します。必ず必要なプロパティはkeydefaultです。

  • key … 他とかぶらない文字列
  • default … 状態の初期値

inputの部分について、最初は何もテキストを入力していない状態なのでdefaultには空文字を指定しています。

次に実際にコンポーネントで使う場合について説明します。

input要素のコンポーネントに「状態」使う

以下のinput要素のコンポーネントで実際にRecoilを使って状態を反映させます。

blocksディレクトリのInputBlock.tsxに以下のコードを書いています。

import { useCallback } from 'react';
import { useRecoilValue} from 'recoil';
import { inputTextState } from '../states/inputTextState';

import classes from './InputBlock.module.scss';

export const InputBlock = () => {
  const inputText = useRecoilValue(inputTextState);
  const setInputText = useSetRecoilState(inputTextState);
 
  const onChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setInputText(event.target.value);
    },
    [setInputText]
  );
 
  return (
    <div className={classes.block}>
      <input type="text" value={inputText} onChange={onChange} className={classes.input} />
    </div>
  );
};

Recoilでは今の状態を取得するにはuseRecoilValueを使い、状態を変更するときはuseSetRecoilStateを使います。

  • useRecoilValue … 今の状態を取得する
  • useSetRecoilState … 状態を変更する

各々を変数に代入してしまえばあとはuseStateと使い方は同じです。

登録したタスクの状態を管理する

input要素でテキストを入力したあと、「登録」ボタンをクリックするとタスクが追加され、タスクの個数も表示されます。

statesディレクトリのaddTextState.tsに書いています。

import { atom, selector } from 'recoil';
import { Items } from '../types/items';

export const addTextState = atom<Array<Items>>({
  key: 'addTextState',
  default: [],
});

export const addTextStateLength = selector<number>({
  key: 'addTextStateLength',
  get: ({ get }) => {
    const addTextNumber: Array<Items> = get(addTextState);
    return addTextNumber?.length || 0;
  },
});

atomの部分では「登録」ボタンを押したときに、タスクを追加していきたいのでdefaltには配列を指定します。

atomは状態を管理するものですが、9行目のselectorは状態を取得して加工するために使います。今回はgetでaddTextStateの値を取得して、配列の中にいくつ値があるかreturnで返しています。

次に実際にコンポーネントで使う場合について説明します。

input要素のコンポーネントに「状態」を追加

「登録」ボタンを押すことでinput要素に書かれたテキストをタスクとして表示させていきたいのでButton要素を追加して、クリックイベントを設定します。

blocksディレクトリのInputBlock.tsxに追加して書いていきます。

import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { addTextState } from '../states/addTextState';
import { inputTextState } from '../states/inputTextState';
import classes from './InputBlock.module.scss';

const getKey = () => Math.random().toString(32).substring(2); // 0〜1未満の乱数字を取得して、数字を32進法に文字列に変換。前から3番目から文字を抽出

export const InputBlock = () => {
  const inputText = useRecoilValue(inputTextState);
  const setInputText = useSetRecoilState(inputTextState);
  const addText = useRecoilValue(addTextState);
  const setAddText = useSetRecoilState(addTextState);

  const onChange = useCallback(
    (event: React.ChangeEvent<HTMLInputElement>) => {
      setInputText(event.target.value);
    },
    [setInputText]
  );
  const onClick = () => {
    setAddText([...addText, { id: getKey(), text: inputText }]);
    setInputText('');
  };

  return (
    <div className={classes.block}>
      <input type="text" value={inputText} onChange={onChange} className={classes.input} />
      <button type="button" className={classes.button} onClick={onClick}>
        登録
      </button>
    </div>
  );
};

状態を取得したいときにuseRecoilValueを使い、状態を変更するときはuseSetRecoilStateを使うことを覚えておきましょう。

タスク表示用のコンポーネントに「状態」を使う

以下のようにタスク用のコンポーネントに状態を反映させます。

blocksディレクトリにあるAddBlock.txに書いていきます。

import { useRecoilValue } from 'recoil';
import { addTextState, addTextStateLength } from '../states/addTextState';
import { Items } from '../types/items';
import classes from './AddBlock.module.scss';

export const AddBlock = () => {
  const addText = useRecoilValue(addTextState);
  const addTextLength = useRecoilValue(addTextStateLength);
  return (
    <div className={classes.block}>
      <div className={classes.length}>{addTextLength}個のタスクがあります</div>
      <ul className={classes.list}>
        {addText.map((item: Items) => (
          <li key={item.id} className={classes.item}>
            {item.text}
          </li>
        ))}
      </ul>
    </div>
  );
};

タスクの個数は8行目でuseRecoilValueから受け取り、11行目で表示させています。
タスク自体は7行目でuseRecoilValueから受け取り、13行目でmapを使ったループで表示させています。

状態をアプリ全体で共有するために

input部分とタスク表示部分のコンポーネントができたので、homeディレクトリの中にあるPage.tsxにそれぞれのコンポーネントを読み込んで1つにまとめます。

import { AddBlock } from '../../../components/blocks/AddBlock';
import { InputBlock } from '../../../components/blocks/InputBlock';
import classes from '../../../components/pages/home/Page.module.scss';

export const Page = () => (
  <div className={classes.main}>
    <InputBlock />
    <AddBlock />
  </div>
);

あとはRecoilで定義した状態をアプリ全体で使うために<RecoilRoot>で全体のコンポーネントを囲う必要があります。今回はPage.tsxをApp.tsxに読み込んで使うので、App.tsxは以下のように書けます。

import { RecoilRoot } from 'recoil';
import { Page } from './components/pages/home/Page';

const App = () => (
  <RecoilRoot>
    <Page />
  </RecoilRoot>
);

export default App;

まとめ

今回はReactの状態管理をRecoilを使って行う方法を説明しました。
ざっくりまとめると以下です。

状態はatomを宣言して管理。状態の値を取得する場合はuseRecoilValueを使い、状態を変更したい場合はuseSetRecoilStateを使う。

状態を加工する場合はselectorを宣言し、getを使って状態を取得して何かして加工したらreturnで値を返す。その値を取得したい場合はuseRecoilValueを使う。

アプリ全体で状態を共有する場合はコンポーネントを<RecoilRoot>で囲う必要がある。

---

今回は紹介できませんでしたが、Recoilが持つ便利の機能の中にはatomFamilyやselectorFamilyもあるので覚えておくと良いでしょう。

以下の記事が参考になります。