Recoil + Hooksがいいかもしれない話
TD;DR
- Reduxとかバケツリレーが従来の状態管理だったけどこれからはRecoilだよ
- RecoilをHooksとうまく活用するととても便利だよ
はじめに
皆さんは、Reactの状態管理をどのようにしてますか?
現状の選択肢だと以下が考えられるかと思います。
- State + バケツリレーで頑張る
- Contextで頑張る
- Reduxで頑張る
それぞれ、メリット/デメリットが有るかと思いますが、
世間一般では3のReduxで頑張る
ことが多いのではないでしょうか?
しかし、去年、Recoilが発表されたことで状態管理に関して新たな選択肢が生まれました。
そもそもReduxってなんやねん?
Recoilの説明をする前にReduxについて復習しましょう。
Reduxの特徴を端的に表すとするならば
アプリケーションの状態を一つのStoreで集権的に管理し
なおかつ
データの流れを単方向にする
ことでアプリケーション全体にまたがる状態管理を行うものです。
アプリケーションの状態を一つのStoreで集権的に管理
基本的に、ReactのComponentは、
- 親から受け取った
props
- 自身がもつ
state
しか参照できません。(Contextもありますが割愛します)
そのため、コンポーネント間で状態を共有するには propsを使わねばなりません。
しかしながら、propsによるstateの共有を行うためには次のように実装しなければなりません。
- propsを上から下へ渡すだけとなるコンポーネントが存在してしまう
- 親子関係にないコンポーネント間でstateを共有する場合、それぞれの先祖となるコンポーネントでStateを保つ必要がある
といった問題点があります。
これに対して、Reduxは、Componentとは独立したStoreを用意し、そこでstateの管理を行うことで 上記の問題を解決しています。
データの流れを単方向にする
Stateを集権的に管理するだけがReduxの役割ではありません。 Reduxは、Fluxアーキテクチャを用いることでデータの流れを単方向にもしてくれるのです。 上の図で説明すると
- Viewからイベントが発火し、actionが発行される
- 発行されたActionをもとにReducer内でStateの書き換えが発生する
- 書き換えられた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を使えるようにする
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のメリット
上記のような実装のメリットですが、
- Reduxに比べ実装が肥大化しない
- Hooksベースで簡潔に書ける
といった利点があります。
また、この例では提示してないですが、atomFamily
というAPIを利用することで、
同じIFのStateを複数個別々に作成することも可能です。
それぞれ、別のstateとして扱われるので、同じような挙動をするStateの実装が一つだけでよくなります。
atomFamily
を用いたコードはこちら
ただ、複雑な状態管理の場合は試せてないですが、管理が煩雑となるのでノウハウが貯まるまではRedux
がいいかもしれません。
最後に
Reduxはたしかに、GlobalにStateを扱い、Fluxアーキテクチャによる単方向データフローという点でReactの状態管理に貢献してきました。
ただし、単にStateをGlobalに扱う、と言うにはオーバースペックです。
今回、Recoilによるhooks friendlyな状態管理を紹介しましたが、
- シンプルなもの
- 同じようなIF、挙動をするState
に関してはReduxをRecoilにどんどん置き換えていってもいいかもしれません。