0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JotaiのatomFamilyでハマった話 😵

Posted at

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(別物!)

問題の流れ:

  1. コンポーネントが再レンダリングされる 🔄
  2. initialDataオブジェクトの参照が変わる 🔄
  3. atomFamilyが新しいatomインスタンスを作成 🆕
  4. SearchFormとFormPageで異なるatomを参照する 🤯
  5. 状態が共有されない 💥

解決策

解決策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は過剰でした...

設計を見直すことで、シンプルで予測可能な状態管理ができるようになりました 🎉

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?