具体例で学ぶ Recoil の使い方|Next.js×Konva (vuex思想)
- 真ん中の茶色い丸をクリックすると周囲に丸が増えます
- 左上の四角をクリックすると色が変わります
前提
- Recoil v0.7
- Next.js v12
- React v18
- konva v8 ・・・ Canvas描画ライブラリ
Recoilの概念を理解する
atom
状態管理するデータの単位です。
今回は画面左上の四角の選択状態と、周囲の丸のリストの 2 つを状態管理しています。
selector
セレクター という名前がややこしいですが、atom の状態を映し出すためのものです。
vuex でいうゲッター(getter)と同義です。
Reduxの難点
Reduxは状態管理を行なっているストアがひとつであるがゆえに、アプリケーション上のデータを常に上書きします。たとえばストアの状態を空オブジェクトのみで更新してしまうと、アプリケーション上のデータはすべて消えてしまうのです。
https://ics.media/entry/210224/
この記事ならではの取り組み
Redux が全体で 1 つのストアだという点に対し、Recoil は atom という単位でストアを構成できるとのことから、ストアという概念を明確にソースファイルとして管理し扱うことを目指しました。これは vuex に慣れた人には馴染み深いものでしょう。
実践
実際に動作するソースコードは StackBlitz でご確認いただけますので、ここからはかいつまんで解説します。
導入
アプリケーションの根っこに Recoil を差し込みます。
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
と名付けてコンポーネントから呼び出せるようにしています。
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,
};
コンポーネントからストアを扱う
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 との棲み分け
コンテクストは解説の例でも取り上げられているように、テーマ色を広い範囲に伝搬させる目的で利用するものであり、アプリケーションのデータストアとして扱うものではないと考えています。つまり、併用もアリなのではないかと思います。アプリケーション全体の根っこに割り当てるというよりは、特定コンポーテントの根っこに使うのが良いでしょう。
おわりに
Canvas 描画ライブラリ Konva が気になった方はこちら🤗
Discussion