jiko21’s techblog

Recoil + Hooksがいいかもしれない話

TD;DR

  1. Reduxとかバケツリレーが従来の状態管理だったけどこれからはRecoilだよ
  2. RecoilをHooksとうまく活用するととても便利だよ

はじめに

皆さんは、Reactの状態管理をどのようにしてますか?

現状の選択肢だと以下が考えられるかと思います。

  1. State + バケツリレーで頑張る
  2. Contextで頑張る
  3. Reduxで頑張る

それぞれ、メリット/デメリットが有るかと思いますが、 世間一般では3のReduxで頑張ることが多いのではないでしょうか?

しかし、去年、Recoilが発表されたことで状態管理に関して新たな選択肢が生まれました。

そもそもReduxってなんやねん?

Recoilの説明をする前にReduxについて復習しましょう。

Reduxの特徴を端的に表すとするならば

アプリケーションの状態を一つのStoreで集権的に管理し

なおかつ

データの流れを単方向にする

ことでアプリケーション全体にまたがる状態管理を行うものです。

アプリケーションの状態を一つのStoreで集権的に管理

基本的に、ReactのComponentは、

  • 親から受け取ったprops
  • 自身がもつstate

しか参照できません。(Contextもありますが割愛します)

そのため、コンポーネント間で状態を共有するには propsを使わねばなりません。

しかしながら、propsによるstateの共有を行うためには次のように実装しなければなりません。

  • propsを上から下へ渡すだけとなるコンポーネントが存在してしまう
  • 親子関係にないコンポーネント間でstateを共有する場合、それぞれの先祖となるコンポーネントでStateを保つ必要がある

といった問題点があります。

Reduxの図 これに対して、Reduxは、Componentとは独立したStoreを用意し、そこでstateの管理を行うことで 上記の問題を解決しています。

データの流れを単方向にする

Stateを集権的に管理するだけがReduxの役割ではありません。 Reduxは、Fluxアーキテクチャを用いることでデータの流れを単方向にもしてくれるのです。 Reduxの図 上の図で説明すると

  1. Viewからイベントが発火し、actionが発行される
  2. 発行されたActionをもとにReducer内でStateの書き換えが発生する
  3. 書き換えられたStateがStoreからコンポーネントに反映される

というふうに、Storeの変更からその反映までを単方向のフローで実現してくれます。

そんなReduxの欠点ってなんなの?

上記のように、バケツリレーという課題感に対する、中央集権・単方向フローのReduxのメリットを述べてきました。

そのReduxですが、デメリットが無いとは言い切れないです。

単にStateを変更するだけのコードが誕生してしまう

単にフラグのようなものをon/off(モーダルの表示/非表示とか)を管理するだけのstateがある場合、 それをon/offにするだけにactionsからreducerまで実装されます。

仮にこれをStateだけで実装した場合の実装量と比較すると、必要なコード量がReduxの場合増えてしまいます。

単にコンポーネントをまたいでState管理するのにこのようなアーキテクチャはオーバースペックです。

Recoil

Reduxの概要とその問題点を説明したところでRecoilの説明をしていきます。

Recoilは公式ホームページにも説明があるように、

React用のステート管理ツール

です。

公式のDocumentのmotivationにしっかりと記載されていますが、 現状のReactにおける状態管理の問題点を解決するべく誕生しました。

Recoilでは次のようにStateをatomsとして定義し

export const statusState = atom<IStatus>({
  key: 'statusState',
  default: Status.TODO,
});

次のように呼び出すことでstateとその変更関数を利用できるようになります。

const [status, set] = useRecoilState(statusState);

React hooksを使っているなら、このような記述はhooksのstateのようで使いやすいですね。

Recoil + Hooksでの効率的なGlobal State管理

では、上記のRecoilをよりagressiveに使ってみましょう。 サンプルとして次のような、サンプルとして次のような、Selectボックスを変えると表示されるリストが変更される、 単純なアプリを用意しました。 recoil hooksのアプリ

こちらにコードを上げています。

Recoilを使えるようにする

ReactのRoot Componentに<RecoilRoot>を噛ませます。

import { RecoilRoot } from 'recoil';

function MyApp() {
  return (
    <RecoilRoot>
      ...
    </RecoilRoot>
  );
}

Stateの定義

セレクトボックスの値をatomとして、Stateを作ります。

import { atom } from 'recoil';

export const statusState = atom<IStatus>({
  key: 'statusState',
  default: Status.TODO,
});

RecoilをComponentから呼び出せるようにする

先程用意したStateをComponentから呼び出せるようにします。

次のようなhooksをComponentとは別に用意します。

export const useStatus = () => {
  const [status, set] = useRecoilState(statusState);

  const setStatus = React.useCallback(
    (value: IStatus) => {
      set(value);
    },
    [set],
  );

  return { status, setStatus };
};

あとは、これをSelectBoxから呼び出すだけです。

const TypeSelectForm: React.VFC = () => {
  const { status, setStatus } = useStatus();

  const handleStatus = (value: IStatus) => {
    setStatus(value);
  };

  return (
    <select
      value={status}
      onChange={(e) => {
        handleStatus(e.target.value as IStatus);
      }}
    >
      ...
    </select>
  );
};

ここまででSelectBoxが動くようになります。

上記Stateの変更をhookにしてAPIを呼び出す

このStateの変更時にAPIなどにアクセスしてデータをfetchできるようにします。 こちらはhooksベースだと次のように実装できます。

import { useRecoilValue } from 'recoil';

export const useTodo = () => {
  const status = useRecoilValue(statusState);
  const { data } = useQuery<IResponse>(['todo', status], async () => {
    const { data } = await axios.get<IResponse>(`/api/todo/${status}`);
    return data;
  });
  return data ? data.todos : [];
};

ここで大事なのは、useRecoilValueというapiです。 こちらのAPIを使うことで、Stateだけをatomから取り出すことが可能です。 あとは

  • 取り出したstateをhookしてapiを呼べるようにする
  • apiの値を返す

だけです。

Recoil + Hooksのメリット

上記のような実装のメリットですが、

  1. Reduxに比べ実装が肥大化しない
  2. Hooksベースで簡潔に書ける

といった利点があります。

また、この例では提示してないですが、atomFamilyというAPIを利用することで、 同じIFのStateを複数個別々に作成することも可能です。

それぞれ、別のstateとして扱われるので、同じような挙動をするStateの実装が一つだけでよくなります。

atomFamilyを用いたコードはこちら

ただ、複雑な状態管理の場合は試せてないですが、管理が煩雑となるのでノウハウが貯まるまではReduxがいいかもしれません。

最後に

Reduxはたしかに、GlobalにStateを扱い、Fluxアーキテクチャによる単方向データフローという点でReactの状態管理に貢献してきました。

ただし、単にStateをGlobalに扱う、と言うにはオーバースペックです。

今回、Recoilによるhooks friendlyな状態管理を紹介しましたが、

  • シンプルなもの
  • 同じようなIF、挙動をするState

に関してはReduxをRecoilにどんどん置き換えていってもいいかもしれません。

about author...

Frontend engineer.
loves: TypeScript, React, Node.js

more detail...