🪣
Jotaiのatomを共通コンポーネントで使う際のバケツリレー問題をProviderで解決する
はじめに
Reactでバケツリレーしたくないなーということで状態管理解消のためにJotaiを入れたのちに、そのコンポーネントを共通コンポーネントとして使い回すことになりました
共通コンポーネント側でJotaiのatomをそのまま使うと状態の分離ができないので、
単純な解決方法は結局バケツリレーでatomを渡すことになります(???意味がない)
今回は、この問題をProviderパターンで解決する方法をご紹介します
問題点の具体例
以下のような構造で共通コンポーネントを使用する場合を考えてみましょう:
親コンポーネント(またはページ)
├── CommonComponent (共通コンポーネント その1) (値を更新)
└── CommonComponent (共通コンポーネント その2) (値を更新)
単純にatomを使用すると、以下のような問題が発生します:
- 両方のCommonComponentが同じatomを参照してしまう
- 一方で値を更新すると、もう一方も更新されてしまう
- コンポーネントの独立性が失われる
解決方法:Providerパターンの活用
(Next14のAppRouterつかってます)
1. Providerの実装
まずProviderを用意します
使用するAtomを渡せるようにしつつ、childrenをcompositionするようにします(重要)
import { createContext, ReactNode, useContext } from 'react';
import { Atom,useAtom } from 'jotai';
interface SharedAtomContextProps {
value: number;
setValue: (value: number) => void;
}
const SharedAtomContext = createContext<SharedAtomContextProps | undefined>(undefined);
interface SharedAtomProviderProps {
sharedAtom: Atom<number>;
children: ReactNode;
}
export const SharedAtomProvider = ({ sharedAtom, children }:SharedAtomProviderProps) => {
const [value, setValue] = useAtom(sharedAtom);
return (
<SharedAtomContext.Provider value={{ value, setValue }}>
{children}
</SharedAtomContext.Provider>
);
};
// カスタムフックを使って子孫コンポーネントから状態に簡単にアクセスできるようにします
export const useSharedAtom = (): SharedAtomContextProps => {
const context = useContext(SharedAtomContext);
if (!context) {
throw new Error('useSharedAtom must be used within SharedAtomProvider');
}
return context;
};
2. 独立した状態を持つ共通コンポーネントの使用例
atomsはそれぞれのコンポーネントごとに用意します
import { atom } from 'jotai';
export const sharedAtomA = atom<number>(0);
export const sharedAtomB = atom<number>(0);
共通コンポーネントを使用する際にSharedAtomProviderをかまして、sharedAtomにそれぞれのatomたちを渡します
import { SharedAtomProvider } from '../components/SharedAtomProvider';
import CommonComponent from '../components/CommonComponent';
import { sharedAtomA,sharedAtomB } from './atoms';
import { useAtomValue } from 'jotai';
const PageA = () => {
const valueA = useAtomValue(sharedAtomA)
const valueB = useAtomValue(sharedAtomB)
return (
<div className="m-4">
<p>
ページで親1の値を参照{String(valueA)}
</p>
<p>
ページで親2の値を参照{String(valueB)}
</p>
<SharedAtomProvider sharedAtom={sharedAtomA}>
<h1>親1</h1>
<CommonComponent />
</SharedAtomProvider>
<SharedAtomProvider sharedAtom={sharedAtomB}>
<h1>親2</h1>
<CommonComponent />
</SharedAtomProvider>
</div>
);
};
3. コンポーネントの実装
import { useSharedAtom } from './SharedAtomProvider';
const CommonComponent = () => {
const { value, setValue } = useSharedAtom();
const addNumber = () => {
setValue(value + 1);
}
return (
<div className="m-6 border border-orange-400">
<h3>Grandchild Component</h3>
<p>現在の値: {value}</p>
<button className="m-2 border border-r-amber-600 bg-amber-300" onClick={addNumber}>値を更新</button>
</div>
);
};
なぜこの方法で解決できるのか?
-
状態の分離
- 各
ParentComponent
は、それぞれ異なるProviderでラップされる - 各Providerは独自のatomを受け取る
- 結果として、各インスタンスは独立した状態を持つ
- 各
-
コンポーネントの再利用性
-
ParentComponent
自体は状態を持たない - Providerが状態管理を担当
- 同じコンポーネントを異なる状態で複数回使用可能
-
メリット
-
状態の独立性
- 各インスタンスが独自の状態を持つ
- 他のインスタンスに影響を与えない
-
コードの保守性
- 状態管理のロジックがProviderに集中
- コンポーネントがシンプルになる
-
再利用性の向上
- 同じコンポーネントを異なる状態で使用可能
- コンポーネントの汎用性が高まる
まとめ
共通コンポーネントでJotaiのatomを使用する際は、Providerパターンを活用することで、バケツリレーを防ぎつつ、コンポーネントの独立性を保つことができます。これにより、メンテナンス性の高い、再利用可能なコンポーネントを実現できます。
Discussion