🍡

具体例で学ぶ Recoil の使い方|Next.js×Konva (vuex思想)

2022/10/23に公開

  • 真ん中の茶色い丸をクリックすると周囲に丸が増えます
  • 左上の四角をクリックすると色が変わります

前提

  • Recoil v0.7
  • Next.js v12
  • React v18
  • konva v8 ・・・ Canvas描画ライブラリ

Recoilの概念を理解する

https://recoiljs.org/

atom

状態管理するデータの単位です。
今回は画面左上の四角の選択状態と、周囲の丸のリストの 2 つを状態管理しています。

selector

セレクター という名前がややこしいですが、atom の状態を映し出すためのものです。
vuex でいうゲッター(getter)と同義です。

Reduxの難点

Reduxは状態管理を行なっているストアがひとつであるがゆえに、アプリケーション上のデータを常に上書きします。たとえばストアの状態を空オブジェクトのみで更新してしまうと、アプリケーション上のデータはすべて消えてしまうのです。
https://ics.media/entry/210224/

この記事ならではの取り組み

Redux が全体で 1 つのストアだという点に対し、Recoil は atom という単位でストアを構成できるとのことから、ストアという概念を明確にソースファイルとして管理し扱うことを目指しました。これは vuex に慣れた人には馴染み深いものでしょう。

実践

実際に動作するソースコードは StackBlitz でご確認いただけますので、ここからはかいつまんで解説します。

導入

アプリケーションの根っこに Recoil を差し込みます。

_app.js
import "../styles/globals.css";
import { RecoilRoot } from "recoil";

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

export default MyApp;

状態管理関連はストアにまとめる

今回取り扱う atom は state (状態) として 1 つのソースコードにまとめます。同様にして selector も getter として管理します。これらを 1 つのストアとして $bubble と名付けてコンポーネントから呼び出せるようにしています。

store/bubble.js
import { atom, selector } from "recoil";

// 以下 2 つをステートとしてまとめる
const state = {
  // 丸の色を保持。初期状態はピンク。
  color: atom({ key: "color", default: "pink" }),
  // 丸のデータを保持。複数あるので配列。初めから 3 つある状態でスタート。
  list: atom({
    key: "list",
    default: [
      { id: "abcd1", x: 100, y: 100, r: 10, o: 0.8 },
      { id: "abcd2", x: 300, y: 200, r: 30, o: 0.3 },
      { id: "abcd3", x: 500, y: 400, r: 5, o: 0.6 },
    ],
  }),
};

// ゲッター。atom の値を加工して扱うために定義する。
const getter = {
  // 画面左上のテキスト用。上記の list を常に見ながら、その件数を報告するテキスト。
  countText: selector({
    key: "countText",
    get: ({ get }) => {
      const list = get(state.list); // ←上記 list を参照
      return `Bubble count: ${list.length}`;
    },
  }),
};

// ヘルパー。ストアとして持つべきロジックを定義する場所。
const helper = {
  // 新しい丸を作るときに呼ぶ。
  create() {
    return {
      id: Math.random().toString(36).substring(2), // リストの key に使用する簡易ID生成機構
      x: Math.random() * window.innerWidth,
      y: Math.random() * window.innerHeight,
      r: Math.random() * 50 + 5,
      o: Math.random(),
    };
  },
};

// コンポーネントから呼び出すとき $bubble という名前で使う。'S'tore と $ を掛けている。
export const $bubble = {
  state,
  getter,
  helper,
};

コンポーネントからストアを扱う

components/Stage.js
import { Stage, Layer, Text } from "react-konva";
import { useRecoilState, useRecoilValue } from "recoil";
import Trunk from "./Trunk";
import Bubble from "./Bubble";
import Seasons from "./Seasons";
import { $bubble } from "../store/bubble"; // ストアの呼び出し

const StageComponent = () => {
  // atom の "list" を参照し、データとそれに対する変更を行う関数を入手。
  const [list, setList] = useRecoilState($bubble.state.list);
  // selector を取り出す。あらかじめ加工された表示用テキスト。
  const countText = useRecoilValue($bubble.getter.countText);

  const bubbleNodes = list.map((o) => {
    return <Bubble key={o.id} x={o.x} y={o.y} r={o.r} o={o.o} />;
  });

  const onClick = () => {
    // 新しい丸を入手。生成ロジックはストア側に閉じ込めている。
    const one = $bubble.helper.create();
    // useRecoilStateで入手した変更用関数を使って新しい丸を追加して全部を再反映。
    setList([...list, one]);
  };

  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        {bubbleNodes}
	{/* 表示用テキストを指定座標に設置 */}
        <Text x={10} y={10} text={countText} />
        <Seasons />
	{/* クリックして丸が増える関数を実行 */}
        <Trunk onClick={onClick} />
      </Layer>
    </Stage>
  );
};

export default StageComponent;

制約

ストアのデータである atom の状態を変えるために使う useRecoilState から取り出した関数や useSetRecoilState といった API は、コンポーネント側から呼び出さなければならないという制約があります。可能であればストア内に action という領域を定義し、それを呼べば状態が変化する処理を用意したかったのですが、現状ではできないようです。

補足

Context API との棲み分け

コンテクストは解説の例でも取り上げられているように、テーマ色を広い範囲に伝搬させる目的で利用するものであり、アプリケーションのデータストアとして扱うものではないと考えています。つまり、併用もアリなのではないかと思います。アプリケーション全体の根っこに割り当てるというよりは、特定コンポーテントの根っこに使うのが良いでしょう。

https://ja.reactjs.org/docs/context.html

おわりに

Canvas 描画ライブラリ Konva が気になった方はこちら🤗

https://zenn.dev/syon/scraps/f8c538cccd64c3

Discussion