Recoilの仕組みと使い方

RecoilはReactのグローバルの状態管理ライブラリーで、Reduxとの比較では簡単で使いやすのが特徴です。FaceBook(Meta)が2020年に発表し、2022年1月にはRecoil 0.6がリリースされました。筆者同様2018年-2020年頃にReactを始めた方は、UseStateでは足りない場合はReduxを避けて通れなかった訳ですが、2022年以降にReactを学び始めた方は、関わるプロジェクトの事情でやむを得ない場合を除き、Reduxのことは忘れて、Recoilを学べばいいと思います(個人的にはReduxはもはや不要とすら思える)。実際に利用者も急激に増えており、npmの週間ダウンロード数は2021年3月の週間約75,000から、2022年3月は週間200,000を超えるようになりました。本稿ではRecoilの仕組み・初期設定・使い方を詳しく解説いたします。初期設定は、ReactとNext.jsの両方で解説いたします。


Recoilの主な特徴:

  • Reduxに比べて構造が単純で扱いやすい(DispatchとReducer不要)
  • Recoil 0.5の辺りから動作が安定、信頼性高まる(正式版が近いと思われる)
  • UseStateに近い感覚で使える
  • 状態の更新が生じると即時にレンダリングされる
  • atomの柔軟性が高い
  • Next.js 13のpages dir構成で動く(Next.js 13のapp dir構成では動きません)

Recoilの弱み:

  • 正式版ではない(2022年3月現在)

本稿で使用したバージョン:

  • React: 17.0.2
  • Next: 12.1.0 (13でも動きます!)
  • Recoil: 0.6.1


Recoilの前提知識 – UseStateの仕組み

App.jsを上位コンポーネントとして、その下にSidebar.jsとCenter.jsが並ぶ単純な構造を考えます。UseStateの場合は下記の通り、設定したコンポーネントの内部で共有関係が閉じているので、コンポーネントを跨いで状態を共有することができません(例:Sidebarでクリックした内容をCenterに反映できない)。

The structure of UseState

最上位コンポーネントに全てのUserStateを記述し、propsを介したバケツリレーのようにするとコンポーネントを跨いだ共有が可能ですが、コンポーネントが3層以上になると中間層のコンポーネント(下記ではCenter.js)もpropsを中継する必要があり非効率です(これをするなら2層が限界)。

Global scope UseState

Recoilの仕組み

Recoilの場合は、状態の共有関係がコンポーネントを超えてグローバルです。状態の格納先はatomと呼びます。コンポーネントとatomが直接接続しているので、コンポーネントが3層以上になったとしても複雑になりません。また、Reduxとは異なりDispatch-Reducerのような中間操作がなく、コンポーネントから直接atomにアクセスできる他、UseRecoilStateだけで状態を設定・更新することも読むこともできます。Reduxに置き換えると、Actionだけで直接stateを更新できるイメージです。

The structure of Recoil

Recoilのインストールと初期設定

既存のReact又はNext.jsのプロジェクトにnpmを使ってインストールします。npmの方は上段、yarnの方は下段です。

npm install recoil
yarn add recoil

インストールの後に、最上位コンポーネントを<RecoilRoot></RecoilRoot>でラップします。Reactの場合はApp.js又はindex.js、Next.jsの場合は_app.jsでこの設定をします。なお、この最上位コンポーネントでは、後述のuseRecoilStateやuseRecoilValueを使って状態を呼び出すことはできません。

Reactの場合

下記はApp.jsで設定した事例です。コンポーネントは先ほどの例にならい、<Sidebar />と<Center />が並列に並んでいるとし、両者を包むように<RecoilRoot>を配置します。両者が包含されてさえいればいいので、外側のdivタグのさらに外側に配置しても構いません。また、場合によっては不要なdivタグを消し、<RecoilRoot>だけを残しても問題なく動きます。

App.js

import React from "react";
import Center from "./components/Center";
import Sidebar from "./components/Sidebar";
import { RecoilRoot } from "recoil";

function App() {
return (
< div className="App">
< RecoilRoot>
<SideBar />
<Center />
</ RecoilRoot>
</ div>
);
}

export default App;

App.jsをクリーンに保ちたい場合は、index.jsで設定しても問題なく動きます。この場合はindex.jsで<App />のすぐ外側を包むように<RecoilRoot>を配置します。ReccoilはReactに依存していますので、<React.StrictMode>タグの外側には配置せず、その内側に<RecoilRoot>を設置するのが無難です(なお、外側に配置しても動きます)。

Next.jsの場合

Next.jsのpages dir構成の場合、root表示を担うindex.jsが読み込まれる前に、_app.jxで表示に必要な前処理を行うことから(詳細はこちらで解説)、Recoilの設定は_app.jsで行います。index.jsではエラーになります。

_app.js

import "../styles/global.css";
import { RecoilRoot } from "recoil";

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

export default MyApp;

なお、Next.jsの公式github内にある主なライブラリーの初期設定を済ませたwith-シリーズの中にも、with-recoilもありますが、Recoilは設定がとても簡単ですので、立ち上げ時に1回しか使えないwithシリーズはapollo、styled componentsなど手間がかかる他のライブラリー組み込みのために使うのがいいと思います(ここで使うのはもったいない)。

他のライブラリーとラップが重なる場合

例えばReactであればRooter、NextであればMaterial-UI, Next-Authなど、Recoilと同様に上位コンポーネントでのラップを要するライブラリーは多いと思います。このようにラップの設定が重なる場合は、RouterとRecoilのように両者に一見無関係な場合でも慎重に順番を決める必要があります。RouterとRecoilの例では、<RecoilRoot>を最も外側に配置する場合(下記の上段)、また、<RecoilRoot>を<BrowserRoot>と<Routes>の間に挟む場合(同中段)は動きますが、<RecoilRoot>を最も内側、すなわち<Router>の内側に配置する(同下段)と正常に動作しません。

atomファイルの作成 – 以下はReactとNext.jsで共通

続いて先ほど説明したatomを格納するフォルダとjsファイルを作成します。設置する場所は任意ですが、慣例上フォルダ名はatoms又はatomとし、ファイル名の語尾はAtom.jsとします。atomは概念上は1つですがファイルは複数作成しても問題ありません。ここではCenterAtom.jsとうファイル名します。CenterAtom.jsを作成したら、(1)atomをrecoilからインポートし、(2)任意の名称でatomオブジェクトを作成します(下記の例ではcenterState)。atomオブジェクトには(3)グローバルで一意に定まるキー”key”と、(4)初期デフォルト値”default”を設定します(下記の例では空の文字列)。atomの初期デフォルト値はUseState()の括弧内で定める初期値と同じ感覚ですので、具体的な使用ケースに即して決めます。最後に(5)atomオブジェクトをexportします。

同じatomファイル内で複数のatomオブジェクトが作成可能です。また、(3)で定める一意のキーはたいてい(2)で定めたatomオブジェクトの名称と一致させます(すなわちatomオブジェクトの名称をプロジェクト内で一意にします)。

CenterAtom.js

import { atom } from "recoil";

export const centerState = atom({
key: "centerState",
default: "",
});

atomの使い方(atomへのアクセス)

atomへの書き込み・更新

ReactまたはNext.js内のコンポーネントで先ほど作成したatomへの書き込み・更新をするにはuseRecoilState() hooks(関数)を使用します。useRecoilState()の引数は、atomファイルからimportするatomオブジェクトです。下記の事例では3行目でcenterStateをインポートしています。useRecoilState()からの出力はuseState()同様に角括弧で受けて、角括弧内の1番目に変数名(下記ではcenter)、2番目に同変数を更新するための関数名(下記ではsetCenter)を記述します。下記の例ではbuttonをクリックすると、centerStateのatomに”Pressed Center”が格納されるようにしています。ボタンを押した直後に、<p>タグに”Pressed Center”が表示されます(同一コンポーネント内の読み込み)。

Center.js

import React from "react";
import { useRecoilState } from "recoil";
import { centerState } from "./atom/CenterAtom";

function Center() {
const [ center, setCenter ] = useRecoilState(centerState);
return (
< div>
< button
onClick={() => {
setCenter("Pressed Center");
})
>
Press here
</ button>
< p>{center}</ p>
</ div>
);
};

export default Center;

別のコンポネントからのatomの読み込み

atomを書き込み・更新をしたコンポーネントとは別のコンポネントからatomを読み込むには、useRecoilValue()かuseRecoilState()を用います。前者の場合は読み込みのみ、後者の場合は更新も可能です。後者の使い方は上述の同一コンポーネント内の事例と同じなので、ここで前者の使い方をご紹介します。コンポーネントはCenter.jsとは別のSidebar.jsとします。

冒頭ではuseRecoilValueと目的のatomオブジェクトcenterStateをインポートします。関数コンポネント内の冒頭では、useRecoilValue関数を使用し、引数はcenterStateとし、出力を任意の変数(ここではcenter)で受けます。JSXではただcenterの内容を表示させています。上述のCenterコンポーネントでボタンをクリックすると、こちらのSidebarコンポーネントにもその内容が反映されます。

Sidebar.js

import React from "react";
import { useRecoilValue } from "recoil";
import { centerState } from "./atom/CenterAtom";

function Sidebar() {
const center = useRecoilValue(centerState);
return (
< div>{center}</ div>
);
};

export default Sidebar;

Recoilには他にもselectorというatomの更新と内容を連動(同期)させる関数があります。useEffectの第二引数にatomオブジェクトを持たせた場合の動きと似ており、useEffectでも代用可能です。selectorについてはuseEffectにはない特徴が見えてきた段階でまたご紹介します。今日も最後まで読んで頂きありがとうございました。