🪣

Jotaiのatomを共通コンポーネントで使う際のバケツリレー問題をProviderで解決する

2024/11/01に公開

はじめに

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>
  );
};

なぜこの方法で解決できるのか?

  1. 状態の分離

    • ParentComponentは、それぞれ異なるProviderでラップされる
    • 各Providerは独自のatomを受け取る
    • 結果として、各インスタンスは独立した状態を持つ
  2. コンポーネントの再利用性

    • ParentComponent自体は状態を持たない
    • Providerが状態管理を担当
    • 同じコンポーネントを異なる状態で複数回使用可能

メリット

  1. 状態の独立性

    • 各インスタンスが独自の状態を持つ
    • 他のインスタンスに影響を与えない
  2. コードの保守性

    • 状態管理のロジックがProviderに集中
    • コンポーネントがシンプルになる
  3. 再利用性の向上

    • 同じコンポーネントを異なる状態で使用可能
    • コンポーネントの汎用性が高まる

まとめ

共通コンポーネントでJotaiのatomを使用する際は、Providerパターンを活用することで、バケツリレーを防ぎつつ、コンポーネントの独立性を保つことができます。これにより、メンテナンス性の高い、再利用可能なコンポーネントを実現できます。

Discussion