atomとatomFamilyの基本的な違い
atom
const counterAtom = atom(0);
const Counter = () => {
const [count, setCount] = useAtom(counterAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
};
アプリ全体で同じ状態を共有する。シンプルで分かりやすい!
atomFamily
const counterAtomFamily = atomFamily((id: string) => atom(0));
const Counter = ({ userId }: { userId: string }) => {
const counterAtom = counterAtomFamily(userId);
const [count, setCount] = useAtom(counterAtom);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
};
key ごとに独立した状態を管理する。複数のカウンターが別々に動作する。
実際にハマったケース
フォームの状態管理で、単一の状態を管理したいだけなのにatomFamilyを使ってしまった。
// 問題のあるコード ❌
type FormData = { id: string; name: string; status: string };
const formAtomFamily = atomFamily((initialData: FormData) => atom(initialData));
const FormPage = ({ initialData }) => {
const formAtom = formAtomFamily(initialData); // オブジェクトをキーに使用
const [form, setForm] = useAtom(formAtom);
return (
<div>
<SearchForm setForm={setForm} /> {/* この更新が反映されない! */}
</div>
);
};
なぜ動かないのか
atomFamilyは、キーが変わるたびに新しいatomインスタンスを作成する。
const data1 = { id: "1", name: "test" };
const data2 = { id: "1", name: "test" }; // 同じ内容だけど...
console.log(data1 === data2); // false(参照が違う)😱
// atomFamilyは参照で判断するので
const atom1 = formAtomFamily(data1); // atomインスタンス A
const atom2 = formAtomFamily(data2); // atomインスタンス B(別物!)
問題の流れ:
- コンポーネントが再レンダリングされる 🔄
-
initialData
オブジェクトの参照が変わる 🔄 - atomFamilyが新しいatomインスタンスを作成 🆕
- SearchFormとFormPageで異なるatomを参照する 🤯
- 状態が共有されない 💥
解決策
解決策1:atomを使う(推奨)
単一の状態管理なら、atomで十分。
const formAtom = atom<FormData | null>(null);
export const useFormData = (initialData: FormData) => {
const [form, setForm] = useAtom(formAtom);
useEffect(() => {
setForm(initialData);
}, []); // 初回のみ実行
return { form, setForm };
};
解決策2:atomFamilyなら安定したキーを使う
どうしてもatomFamilyを使いたい場合は、キーを固定値にする。
const formAtomFamily = atomFamily((formId: string) =>
atom<FormData | null>(null)
);
export const useFormData = (formId: string, initialData: FormData) => {
const formAtom = formAtomFamily(formId);
const [form, setForm] = useAtom(formAtom);
useEffect(() => {
if (initialData) {
setForm(initialData);
}
}, []); // 初回のみ実行
return { form, setForm };
};
使い分けの基準
atomを使う場面
// グローバルな単一状態
const userAtom = atom<User | null>(null);
const themeAtom = atom<'light' | 'dark'>('light');
const modalAtom = atom(false);
atomFamilyを使う場面
// 複数の独立した状態が必要
const userProfileAtomFamily = atomFamily((userId: string) =>
atom<Profile | null>(null)
);
const chatRoomAtomFamily = atomFamily((roomId: string) =>
atom<Message[]>([])
);
const todoListAtomFamily = atomFamily((listId: string) =>
atom<Todo[]>([])
);
やってはいけないこと
// ❌ オブジェクトをキーにする
const badAtomFamily = atomFamily((obj: FormData) => atom(obj));
// ❌ 単一状態でatomFamilyを使う
const unnecessaryAtomFamily = atomFamily(() => atom(singleState));
まとめ
-
単一の状態管理なら普通の
atom
を使う -
複数の独立した状態が必要な時だけ
atomFamily
を使う - atomFamilyのキーには必ず固定値を使う
- atomFamilyのキーにオブジェクトを使うと参照問題でハマる
今回のケースでは、1つの状態しか持ちたくないのに、atomFamilyは過剰でした...
設計を見直すことで、シンプルで予測可能な状態管理ができるようになりました 🎉