Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

Recoilを触ってみた話

はじめに

はじめまして。22年新卒入社のsugiyamaです。 先日中学校からの友人と三人で勢いで真冬に富士山の麓でグランピングをしてきたらBBQ中、あまりの寒さに焚き火のそばから離れられませんでした。寝るのはコテージだったので寒くありませんでしたが、ゆるキャンみたいな雰囲気をイメージしてたら全然ゆるくなかったキャンプでしたが楽しかったです。

この記事はGunosy Advent Calendar 2022 19日目の記事になります。 昨日の記事はかとうさんのこちらの記事になります。

Recoilとは

Meta(Facebook)が開発している React の状態管理ライブラリです。

またRecoilは2022年12月19日現在、experimentalであるため今後仕様が大きく変わる可能性があります。ご了承ください。

↓公式ドキュメント

recoiljs.org

↓GitHub

github.com

特徴

  • Recoilでは、stateを裏で集約して管理していますが、それは暗黙的に行われており表面上は直接共有することができます。
  • 集約して管理していない為Atoms(後ほど説明します)が更新された場合全体が再レンダリングされず、更新されたAtoms及びSelectorを使用しているcomponent下のみ再レンダリングされます。

AtomsとSelector

Atoms

Atomsはアプリケーションの状態を保持することができます。

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

key :
Atomsを内部的に識別するためのものです。keyはアプリケーション全体で一意である必要があります。

default :
Atomsの初期値を定義します。

Selector(options)

Selector は以下のような特徴があります。

  • SelectorはAtoms、他Selectorを受け取る関数として定義することができます。
  • Selectorは依存しているAtomsやSelectorが更新された際自動で更新がかかります。

今回は上で定義したnumAtomの値に +1した値を取得できるSelectorを定義しています。

export const addNum = selector({
  key: "addNumAtom",
  get: ({ get }) => {
    const num = get(numAtom);  
    return num + 1;
  },
});

key :

Selectorでも同様にを内部的に識別するためのものです。keyはアプリケーション全体で一意である必要があります。

get :
別のAtomsやSelectorの値を計算する関数です。主に値(numberやstring、object等)やPromise、Loadable、atomやSelectorを返すことができます。

また selector は set を定義することで、他 atom や selector の更新が可能です。

Recoil 使い方

ここでは簡単に Recoil を導入する方法を示します。

開発環境

  • React 18.2.0
  • Recoil 0.7.6
  • Node 19.2.0

上記の環境で動作させた前提で説明していきます。

今回は次の内容を紹介します。

  • RecoilRoot の設定方法
  • データを永続化の方法
  • Atoms や Selector の定義方法
  • Atoms から state の取得方法

さらに詳しく知りたい方や非同期処理などについて知りたい方は公式ドキュメントを参照してください。

↓公式ドキュメントの非同期処理のページ。

recoiljs.org

RecoilRoot の設定

import { RecoilRoot } from "recoil";
import App from "./App";

function Root() {
  return (
    <RecoilRoot>
      <App />
    </RecoilRoot>
  );
}

export default Root;

RecoilRootはRecoil hooksを使用するすべてのcomponentの親ではなくてはなりません。 

しかしRecoilRootを複数使用しAtomsの状態をそれぞれのstoreに保持することも可能です。しかしその場合、それぞれのRecoilRoot毎に異なる値を持ちます。

データを永続化する

今回説明した方法だとページをリロードした際に値がリセットされてしまいます。なのでrecoilPersistを使用することで永続化することができます。

github.com

recoilPersistはデフォルトでlocalStorageが使用されます。

import { atom } from "recoil";
+ import { recoilPersist } from "recoil-persist";

+ const { persistAtom } = recoilPersist();

export const parent1Atom = atom({
  key: "parent1Atom",
  default: 0,
+ effects_UNSTABLE: [persistAtom],
});

Atomsを定義する際、effects_UNSTABLEを追加しrecoilPersist() で定義した値を入れてあげるだけです。
これだけでAtomsの値を永続化することができます。

component等で定義したAtomsからstateを取得する

Atomsからstateを取得する方法は二つあります。

1つめ

1つめはuseRecoilStateを使用する方法です。reactのuseStateとほぼ同じように使用することができます。

const [num, setNum] = useRecoilState(numAtom);

numにはnumAtomと定義したAtomsの値が取得できます。

setNumにはnumAtomの状態を更新するsetter関数が取得できます。

2つめ

二つめはAtomsの値とsetter関数をそれぞれ取得する方法です。

const num = useRecoilValue(numAtom);
const setNum = useSetRecoilState(numAtom);

useRecoilValueはnumAtomと定義したAtomsの値が取得できます。

useSetRecoilStateはnumAtomの状態を更新するsetter関数が取得できます。

レンダリング周りの検証

今回検証したリポジトリはこちら

今回は再レンダリングされたか簡単に確認するために、再レンダリングされたら文字の色が変更されるようにしています。

条件設定

今回は上の画像のように4つのコンポーネントおよびそれぞれのAtomsを定義しました。

親1

parent1AtomというAtomsのみ参照

import { useRecoilState } from "recoil";
import { parent1Atom } from "./atom";
import Children1 from "./Children1";
import RecoilCount from "./recoilCounter";

const Parent1: React.FC = () => {
  const [num, setNum] = useRecoilState(parent1Atom);

  const getColor = () => Math.floor(Math.random() * 255);
  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  };
  return (
    <div>
      <h2 style={style}>{num}</h2>
      <RecoilCount count={num} setCount={setNum} />
      <p>子1</p>
      <Children1 />
    </div>
  );
};
export default Parent1;

親1の子componentである子1

children1AtomというAtomsのみ参照

import { useRecoilState } from "recoil";
import { children1Atom } from "./atom";
import RecoilCount from "./recoilCounter";

const Children1: React.FC = () => {
  const [num, setNum] = useRecoilState(children1Atom);

  const getColor = () => Math.floor(Math.random() * 255);

  const style = {
    color: `rgb(${getColor()},${getColor()},${getColor()})`,
  };
  return (
    <div>
      <h2 style={style}>{num}</h2>
      <RecoilCount count={num} setCount={setNum} />
    </div>
  );
};
export default Children1;

親2

parent2AtomというAtomsのみ参照しています。

コードは親1とほぼ同じでAtomsの参照先がparent1Atomからparent2Atomに変わるだけなので今回は省略します。

親2の子componentである子2

children2AtomというAtomsのみ参照しています。

こちらもコードは親1とほぼ同じでAtomsの参照先がchildren1Atomからchildren2Atomに変わるだけなので今回は省略します。

Atoms定義は下のようになります。

export const parent1Atom = atom({
  key: "parent1Atom",
  default: 0,
  effects_UNSTABLE: [persistAtom],
});

export const parent2Atom = atom({
  key: "parent2Atom",
  default: 0,
  effects_UNSTABLE: [persistAtom],
});

export const children1Atom = atom({
  key: "children1Atom",
  default: 0,
  effects_UNSTABLE: [persistAtom],
});

export const children2Atom = atom({
  key: "children2Atom",
  default: 0,
  effects_UNSTABLE: [persistAtom],
});

今回作成したものは以下のように動作します。

  • 下のIncrementのボタンを押すとAtomsの数字が 1 つずつ加算される
  • Increment ボタンが押された時 atom が更新されコンポーネントが再レンダリングされる
  • コンポーネントが再レンダリングされた際それぞれの下の数字の色がランダムで変わるようにしている

検証 1: 親の再レンダリングと子関係の検証

左上の親1のボタンを押しAtomsを更新する

Before

After

親1のAtomsが更新されたことによって親1とその子componentである子1が再レンダリングされました。 ですが親2及びその子componentの子2は親1が変更したAtomsを参照していない為再レンダリングされませんでした。

検証 2: Atomsの更新における再レンダリングの検証

子2で子1で参照しているAtomsの値を取得し表示してみる

子2の下にuseRecoilValueを使用して子1で取得 および更新しているchildren1Atomを表示させます 。 それでは子1のボタンを押してみます。

ボタンを押すと、children1Atomの値が更新されました。それによってchildren1Atomを参照している子1、子2が再レンダリングされたのが確認できました。

おわりに

今回初めてRecoilに触れてみましたがReduxと比べややこしい概念が少なくかなり理解しやすかったです。現状はまだexperimental なので仕様が大きく変更されるかもしれませんが使ってみるのも良さそうだなーって感じました。
明日はHAMASHITAさんの記事になります!