こんにちは、 tbaba です。元々 Rubyist として入社していますが、ここ2〜3年はフロントエンド力の向上にも力を注いでおります。
突然ですが、React で状態を管理する時に何を使っていますか?クラスコンポーネントにしてクラスに状態をもたせている、Redux を使って管理している、React Hooks で管理している、などなど色々な選択肢があるかと思います。
そんな中で自分たちのチームは、現在社内向けのアプリケーションにおいて、フロントエンド開発をする際に Recoil という状態管理ライブラリを使うことが多いです。そこで、今日は「なんでそれ使うの」「何が便利なの」みたいな話ができれば良いなと思います。
先に言っておくと、自分のスキルセットとしては「 TypeScript を利用した開発2年目」「React を利用した開発3年目」「基本は Ruby on Rails が得意なバックエンドエンジニア」です。なので React の状態管理について理解の浅い点があるかもしれませんが、ご了承ください。
なぜ Recoil を使うのか
端的に言うと、Redux を使うことに疲れたからです。
Redux は React コンポーネントの状態 (state) を上手く管理するためのフレームワークです。元はデータの状態変化の流れを一本化しようというアーキテクチャの派生としてリリースされて、今では React アプリケーションの状態管理のデファクトスタンダードと言っても過言ではないくらい、様々なプロダクトで利用されていることと思います。
しかし、後述しますが Redux は大きなアプリケーションを作る際には更新処理などを整理してわかりやすく配置することができますが、その特性上複雑であったり、パッと見では理解しづらいところがあります。僕自身、Redux は書けるといえば書けるけど、あまり書きたくないな、と感じてしまいます。特に、よくある「マルチページアプリケーションの画面の一部を React 化する」と言った小規模な開発では使いたくないというのが本音です。
そんな時に React Hooks や Recoil を使うと、驚くほどシンプルにコードを書くことが出来ます。以下、その理由を書いていきます。
理由その1: 状態の持ち方が楽
例えば Recoil では、とある状態を持つときに以下のように設定します。
import React from "react"; import { atom, MutableSnapshot, RecoilRoot, useRecoilValue } from "recoil"; // Todo の id をインクリメントするやつ let id = 0; function getId() { return id++; } // Todo の型 type Todo = { id: number; content: string; isCompleted: boolean; }; // Todo の状態の定義 const todosState = atom<Todo[]>({ key: "state/todos", default: [], }); const Todos = () => { const todos = useRecoilValue(todosState); const contents = todos.map((todo) => ( <div key={`todo-${todo.id}`}>{todo.content}</div> )); return <>{contents}</>; }; // 初期値のセット const initialize = ({ set }: MutableSnapshot) => { set(todosState, [ { id: getId(), content: "ご飯を買ってくる", isCompleted: false, }, { id: getId(), content: "手を洗う", isCompleted: false, }, ]); }; export default function () { return ( <RecoilRoot initializeState={initialize}> <Todos /> </RecoilRoot> ); }
# RecoilRoot を置いて、その中で Recoil の関数を呼び出すようにしましょう、とかの基本的なことについては、公式ドキュメントを参考にしていただけると分かりやすいと思います。
key
は atom
を状態を示す一意の値、 default
はデフォルト値です。
つまり、デフォルト値を自由に設定できるのが大変便利な上、 TypeScript で型を指定してあげることで、型保証も簡単に出来ます。
同じことを Redux を利用すると、「 reducer
を作って」「 store
を作って」「 connect
して」など、色々とやることが多いですよね。そういうのはキレイさっぱりスキップできます。
状態の更新ですら、Redux なら「 action
を作って」「 import
して」「 dispatch
に食わせて」「 connect
して」といった風にしなきゃいけないところ、Recoil だと「 useSetRecoilState
を呼び出す」で終わりです。
理由その2: 初期化がすごく分かりやすい
Redux や Hooks などでも初期化はできます。しかし、そのために Provider に store を食わせたり、そのために初期値のオブジェクトをデフォルト値のオブジェクトと手動でマージしたりとやることが多い印象です。 いちいち reducer 作ったりして大掛かりになるのも面倒くさいですね。
ところが、 Recoil はそれすら簡単です。
import React from ‘react’; import { RecoilRoot, todosState } from ‘recoil’; import { Todo } from ‘./models’; import { todosState } from ‘./modules’; const initialize = ({ todos }: Todo[]) => ({ set }: MutableSnapshot) => { set(todosState, todos); }; export default function (props) { return ( <RecoilRoot initializeState={initialize(props)}> <Todos /> </RecoilRoot> ); }
このように App
というルートコンポーネントが持っている初期値を、 RecoilRoot
が持つ initializeState
に食わせるだけで良いのです。その中では set
という関数が使えるため、 atom
と初期値を渡してあげればそれだけで状態として保持してくれます。
もちろん、 initializeState
関数の中で非同期で取得し、それを set
に渡すことも可能です。
非同期の場合は atom
直ではなく selector
を使って状態をセットすることも可能なのですが、それはまた別のときにでも。
そういえば、Redux などでは非同期処理のためにミドルウェアを入れたりしますね。 Recoil の場合は最初から対応しているため、特殊なミドルウェアの導入などは必要ありません。閑話休題。
理由その3: 余計なところで状態を読み込まなくて済む
状態管理、面倒くさいですよね。分かります。いろいろ継ぎ足していったらものすごい大きなオブジェクトが完成して、しかもそれがどっか更新するたびに全部更新されるみたいなことが起こりえます。更新したいのはほんの一部なのに!と思うことがありますね。
Redux だと、大きな reducer を作ってあげたり、複数の reducer を combineReducers
を使って名前空間が区切られている一つのオブジェクトにする、といった風に、少し工夫してあげる必要があります。が、それを理解するのはしんどいです。React Hooks の Context API で頑張る、でも良いのですが、結局複数の Context を管理する必要があって煩雑でした。
それに対して Recoil は簡単です。それぞれの状態を atom
が持っているので、「Aの状態を知りたかったらAの atom を見に行けば解決!」という風にスッキリさせることが可能なのです。
const todosState = atom<Todo[]>({ key: 'state/todos', default: [], }); const loadingState = atom<boolean>({ key: 'state/loading', default: false, }); function ComponentA const todos = useRecoilValue(todosState); const loading = useRecoilValue(loadingState); return ( <> {loading ? <div>Now loading</div> : todos.map((todo) => <div>{todo.content}</div>)} </> ); } function ComponentB // loadingState は見る必要がないので省いて問題がない // ここの todosState は、 ComponentA で見ているものと同じものを見ていることになる const todos = useRecoilValue(todosState); return <>{todos.map((todo) => <div><input type="checkbox" checked={todo.isCompleted} />ステータス</div>)}</>; }
値を参照したいだけなら useRecoilValue
、参照と代入をしたければ useRecoilState
、代入のみしたければ useSetRecoilState
を利用しましょう。
このように、Recoil は一つのオブジェクトに状態を集約させたり、複数の Context を管理したりする必要がありません。 RecoilRoot
コンポーネントの中であれば、自由に呼び出すことができる*1ので、とても便利なのではないでしょうか。
まとめ
以上のように、 Recoil はコンパクトで、状態を管理しやすく、Hooks に慣れていれば使うときにもあまり悩まない大変便利な状態管理ライブラリです。基本コンセプトが「コンポーネント間で共有される状態 (atom) 」と「関数 (selector) 」で、それよりも複雑なことはやろうと思えばできるし用意もある、でも基本この2つでどうにかなるよ、というものなのです。
加えて TypeScript との親和性も非常に高く、ここ最近の Redux に心をやられているという方には大変おすすめとなっております。
ただ、気をつけたほうが良いこともあるので、こちらに書いておきます。
実はまだメジャーリリースがない
この記事を書いている 2021/05/31 現在、最新バージョンが 0.3.1 となっています。つまりまだ大変活発に実装が進んでいる、枯れていないライブラリです。そんな状況なので、いつ API が変わってしまうかもわかりません。
加えて、 description には Recoil is an experimental state management library for React apps.
とあります。 experimental
です。実験的なライブラリなので、無くなる可能性も考慮しながら作る必要があります。もし無くなってしまったっ場合は自分で保守するという強い意志を持ちましょう。
シンプルで自由度が高いがゆえに気をつけたい
atom
を利用することで、シンプルに状態を作ることができるのはここまでで理解していただけたかと思います。
しかし、それゆえにぽんぽん自由に状態を突っ込んでいくと、結局のところ「何がどういう風にデータとして入っているのか」「どう更新処理を入れればよいのか」が分かりづらくなってしまうのは間違いありません。
そういう状態になってしまうのを避けるため、可能な限り型を使って安全に入出力値を定義してあげるのをオススメします。
selector
などを使って非同期に処理を行う際も同様です。
とりあえず社内のアプリから、いかがですか?
いきなり本番環境に投入するの怖いとかがあるのでしたら、社内で使うようなちょっとした便利アプリなんかに Recoil を導入してみるのはいかがでしょうか?
触っていて面白いですし、これまでそこそこちゃんと設計してやらなきゃいけなかった React の状態管理がサクサク進むというアハ体験を感じることができますよ!
人を探しています
そんな弊社ですが、実は開発チームを技術面からリードしてくれるようなエンジニアを募集しています。
2つのプロダクトとチームがあって、どちらかに入ってもりもりと開発を進めたり、技術的な検証やアドバイスをしてくれる、そんな方を探しているところです。
もしご興味があれば一度、以下の求人から「話を聞きたい」などでコンタクトを取っていただけると幸いです。
(ちなみに tbaba は採用にも関わっており、もし選考に進んでいただけるとなればご挨拶する機会もあるかと思います。その時はぜひお喋りしましょう)
*1:実装の中身は React Hooks なので、呼び出しの制約は Hooks に準じるのだけ注意です。