こんにちは!
RevComm に2022年1月に入社したフロントエンドエンジニアの小山 (koji-koji) です。
RevComm では、 React を採用しているサービスの状態管理に Recoil を使っています。今回は Recoil の理解をより深めるために Context と比較してみました。
Context では小さく状態管理できる。 Recoil でもできないか?
Context の状態管理のスコープは <Context.Provider>
で括った対象であり、一方 Recoil では <RecoilRoot>
で括った対象です。
Context では、ルートコンポーネント以外を <Context.Provider>
で括った場合、子コンポーネントでの変更はグローバルには反映されません。
ローカルではないけれど、グローバルというほど大きくなく状態管理をしたいときに使えます。それが Recoil でもできるのか?と考えました。
Recoil では atomFamily()
を使って atom の key を動的に生成する方法があるようです。これはリストを扱うときは良さそうです。一方、リストでない場合もあるので、もう少し調べてみました。
<RecoilRoot>
をネストさせると小さく状態管理できる
<RecoilRoot>
のドキュメント によると、ネストさせるとスコープを調整できることがわかりました。
ルートコンポーネントだけでなく子コンポーネントでも使うと、子コンポーネントの中だけで状態管理ができます。
親コンポーネントで状態を変更しても、子コンポーネントには反映されません。
子コンポーネントで状態を変更しても、親コンポーネントには反映されないという形でスコープが狭まります。
サンプルです。
そこで RecoilRoot のコードを見てみました。 Flow で書かれていますが、あまり知識がなくても読めそうです。
ストアを比較して異なっている場合はネストしているので、処理を分けているという感じです。
なお Recoil は Context を内部的に使っていて、 atom と selector をうまく活用することで再レンダリングを抑えているといった方が正しそうです。 RecoilRoot の挙動についての記事は以上です。 今回はドキュメントを読むのに加えて、実際に挙動を確認したり、コードリーディングをしました。 コードリーディングをすると、やはり理解が深まったり発見があったりしますね!
サンプルコード
function MyApp({ Component, pageProps }: AppProps) {
return (
<RecoilRoot>
<Component {...pageProps} id="modalId" />
</RecoilRoot>
);
}
const ParentComponent: React.FC = () => {
const [sample, setSample] = useRecoilState(sampleState);
return (
<div className="bg-blue-100 p-2">
ParentComponent: {sample}
<button className="ml-3 bg-gray-100 p-3" onClick={() => setSample(sample + 1)}>
ボタン
</button>
<div className="mt-2 flex">
<RecoilRoot>
<NestChildComponentA />
<NestChildComponentB />
</RecoilRoot>
<ChildComponentC />
</div>
</div>
);
};
const NestChildComponentA: React.FC = () => {
const [sample, setSample] = useRecoilState(sampleState);
return (
<div className="bg-pink-300 p-2">
<RecoilRoot>
NestChildComponentA: {sample}
<button className="ml-3 bg-gray-100 p-3" onClick={() => setSample(sample + 1)}>
ボタン
</button>
<NestGrandChildComponent />
</RecoilRoot>
</div>
);
};
const NestChildComponentB: React.FC = () => {
const [sample, setSample] = useRecoilState(sampleState);
return (
<div className="bg-green-300 p-2">
<RecoilRoot>
NestChildComponentB: {sample}
<button className="ml-3 bg-gray-100 p-3" onClick={() => setSample(sample + 1)}>
ボタン
</button>
</RecoilRoot>
</div>
);
};
const ChildComponentC: React.FC = () => {
const [sample, setSample] = useRecoilState(sampleState);
return (
<div className="bg-yellow-300 p-2">
<RecoilRoot>
ChildComponentB: {sample}
<button className="ml-3 bg-gray-100 p-3" onClick={() => setSample(sample + 1)}>
ボタン
</button>
</RecoilRoot>
</div>
);
};
const NestGrandChildComponent: React.FC = () => {
const [sample, setSample] = useRecoilState(sampleState);
return (
<div className="mt-2 bg-blue-300 p-2">
<RecoilRoot>
NestGrandChildComponent: {sample}
<button className="ml-3 bg-gray-100 p-3" onClick={() => setSample(sample + 1)}>
ボタン
</button>
</RecoilRoot>
</div>
);
};
export const sampleState = atom<number>({
key: 'SampleAtom',
default: 0,
});
</div>
<RecoilRoot>
を適切にネストさせることで状態管理のスコープを調整できそうですね。なぜ
<RecoilRoot>
をネストさせるとスコープが閉じるのか<RecoilRoot>
をネストさせたときの挙動はわかったのですが、なぜこうなるのかあまりよくわかりませんでした。
553 行目から RecoilRoot の記述があります(今回参照した Recoil のバージョンは 0.7.2 です)。function RecoilRoot(props: Props): React.Node {
const {override, ...propsExceptOverride} = props;
const ancestorStoreRef = useStoreRef();
if (override === false && ancestorStoreRef.current !== defaultStore) {
// If ancestorStoreRef.current !== defaultStore, it means that this
// RecoilRoot is not nested within another.
return props.children;
}
return <RecoilRoot_INTERNAL {...propsExceptOverride} />;
}
ネストしている場合は、別のストアを作るという処理をしています。Recoil_RecoilRoot.js
の中のコードで気がついたのですが、useContext
を使っていました。
私は Recoil を触る前は、 Context vs Recoil というイメージを持っていました。しかしその理解は少し違っていたみたいです。
今後も気になるところがあったら、どんどんコードリーディングしていきたいです。