📝

PropsなしのReactコンポーネントを過度にネストしなくて済む方法

2023/09/24に公開
2

はじめに

Reactで状態管理のために、Context APIを使っている方はそれなりにいらっしゃると思いますが、よく見かけるのがContext.Providerを内部で使ったコンポーネントをネストしまくって整理しづらくなっているパターンです。

今回はそれをすっきりさせるるための方法をひとつ紹介します。

2024/1/6 追記

結論

コンポーネントを再帰的にまとめる関数を作る

// @/utils/buildProvidersTree.tsx
type ProviderComponent = React.FC<any>;

const buildProvidersTree = (providers: ProviderComponent[]): ProviderComponent => {
    // 基本ケース:ContextProviderが1つしか残っていない場合、それを返して終了する
    if (providers.length === 1) {
        return providers[0];
    }

    // 配列から最初の2つのContextProviderを取り出す
    const FirstProvider = providers.shift();
    const SecondProvider = providers.shift();

    // 十分な数のContextProviderがあるかどうかを確認
    if (FirstProvider === undefined || SecondProvider === undefined) {
        throw new Error('ContextProviderが不足しています');
    }

    // 最初の2つのContextProviderをネストした新しいContextProviderを作成し、再帰する
    return buildProvidersTree([
        ({ children }) => (
            <FirstProvider>
                <SecondProvider>{children}</SecondProvider>
            </FirstProvider>
        ),
        ...providers,
    ]);
};

export default buildProvidersTree;

使用方法

// @/App.tsx
import AuthProvider from '@/providers/AuthProvider/AuthProvider';
import AxiosProvider from '@/providers/AxiosProvider/AxiosProvider';
import BreakpointProvider from '@/providers/BreakpointProvider/BreakpointProvider';
import YourAwesomeProvider1 from '@/providers/YourAwesomeProvider1';
import YourAwesomeProvider2 from '@/providers/YourAwesomeProvider2';
import YourAwesomeProvider3 from '@/providers/YourAwesomeProvider3';
import buildProvidersTree from '@/utils/buildProvidersTree';

const ProviderTree = buildProvidersTree([
    AxiosProvider,
    AuthProvider,
    BreakpointProvider,
    YourAwesomeProvider1,
    YourAwesomeProvider2,
    YourAwesomeProvider3,
]);

const App = () => (
    <ProviderTree>
        <YourAwesomeApp />
    </ProviderTree>
);

export default App;

コンポーネントのネストでよく見かけるパターン

パターン 1

最高階でとりあえずネストしまくってからAppをラップするパターン

// @/App.tsx
import AuthProvider from '@/providers/AuthProvider/AuthProvider';
import AxiosProvider from '@/providers/AxiosProvider/AxiosProvider';
import BreakpointProvider from '@/providers/BreakpointProvider/BreakpointProvider';
import YourAwesomeProvider1 from '@/providers/YourAwesomeProvider1';
import YourAwesomeProvider2 from '@/providers/YourAwesomeProvider2';
import YourAwesomeProvider3 from '@/providers/YourAwesomeProvider3';

const App = () => (
    <AxiosProvider>
        <AuthProvider>
            <BreakpointProvider>
                <YourAwesomeProvider1>
                    <YourAwesomeProvider2>
                        <YourAwesomeProvider3>
                            <YourAwesomeApp />
                        </YourAwesomeProvider3>
                    </YourAwesomeProvider2>
                </YourAwesomeProvider1>
            </BreakpointProvider>
        </AuthProvider>
    </AxiosProvider>
);

export default App;

パターン 2

何とかひとつのファイルにまとめて見た目だけすっきりパターン

コンポーネントをネストしまくるパターンは、コンポーネントをひとつのファイルにまとめるパターンに置き換えることができます。

// @/YourProviders.tsx
import AuthProvider from '@/providers/AuthProvider/AuthProvider';
import AxiosProvider from '@/providers/AxiosProvider/AxiosProvider';
import BreakpointProvider from '@/providers/BreakpointProvider/BreakpointProvider';
import YourAwesomeProvider1 from '@/providers/YourAwesomeProvider1';
import YourAwesomeProvider2 from '@/providers/YourAwesomeProvider2';
import YourAwesomeProvider3 from '@/providers/YourAwesomeProvider3';

const YourProviders: React.FC<{ children: React.ReactNode }> = ({ children }) => (
    <AxiosProvider>
        <AuthProvider>
            <BreakpointProvider>
                <YourAwesomeProvider1>
                    <YourAwesomeProvider2>
                        <YourAwesomeProvider3>{children}</YourAwesomeProvider3>
                    </YourAwesomeProvider2>
                </YourAwesomeProvider1>
            </BreakpointProvider>
        </AuthProvider>
    </AxiosProvider>
);

export default YourProviders;
// @/App.tsx
import YourProviders from './YourProviders';

const App = () => (
    <YourProviders>
        <YourAwesomeApp />
    </YourProviders>
);

これらのパターンの問題点

  1. コンポーネントをネストしまくると、見た目がすっきりしない
  2. どのコンポーネントがどのコンポーネントをラップしているのかがわかりにくい
  3. コンポーネントを追加するたびに、Appをラップしているコンポーネントを追加する必要がある
  4. 大量にコンポーネントがあると、順番を変えたいとかなったらもう大変

解決策

コンポーネントの配列をまとめる関数を作る

// @/utils/buildProvidersTree.tsx
type ProviderComponent = React.FC<any>;

/**
 * ContextProviderを再帰的にまとめる関数
 * @param providers ContextProviderの配列
 * @returns ContextProviderを再帰的にまとめたコンポーネント
 */
const buildProvidersTree = (providers: ProviderComponent[]): ProviderComponent => {
    // 基本ケース:ContextProviderが1つしか残っていない場合、それを返して終了する
    if (providers.length === 1) {
        return providers[0];
    }

    // 配列から最初の2つのContextProviderを取り出す
    const FirstProvider = providers.shift();
    const SecondProvider = providers.shift();

    // 十分な数のContextProviderがあるかどうかを確認
    if (FirstProvider === undefined || SecondProvider === undefined) {
        throw new Error('ContextProviderが不足しています');
    }

    // 最初の2つのContextProviderをネストした新しいContextProviderを作成し、再帰する
    return buildProvidersTree([
        ({ children }) => (
            <FirstProvider>
                <SecondProvider>{children}</SecondProvider>
            </FirstProvider>
        ),
        ...providers,
    ]);
};

export default buildProvidersTree;

使いたいとこで使う

// @/App.tsx
import AuthProvider from '@/providers/AuthProvider/AuthProvider';
import AxiosProvider from '@/providers/AxiosProvider/AxiosProvider';
import BreakpointProvider from '@/providers/BreakpointProvider/BreakpointProvider';
import YourAwesomeProvider1 from '@/providers/YourAwesomeProvider1';
import YourAwesomeProvider2 from '@/providers/YourAwesomeProvider2';
import YourAwesomeProvider3 from '@/providers/YourAwesomeProvider3';
import buildProvidersTree from '@/utils/buildProvidersTree';

const ProviderTree = buildProvidersTree([
    AxiosProvider,
    AuthProvider,
    BreakpointProvider,
    YourAwesomeProvider1,
    YourAwesomeProvider2,
    YourAwesomeProvider3,
]);

const App = () => (
    <ProviderTree>
        <YourAwesomeApp />
    </ProviderTree>
);

export default App;

こうすると、見た目も順番もすっきりしますので、私はこの方法を推奨しています。

参考

https://react.dev/learn/passing-data-deeply-with-context
https://alexkorep.com/react/react-many-context-providers-tree/

更新履歴

  • 2024/1/6: 誤解を招く表現を修正し、〇〇Providerという名前のコンポーネントについての説明を追加
  • 2023/9/24: 初稿

Discussion

クロパンダクロパンダ

記事タイトル通りのことができてないと思います。

普通、MyContext.Providerにはvalueを渡すはずです(じゃないと意味がなさすぎる)。ですが buildProvidersTree 関数では MyContext.Provider へ value を渡すことができず、機能として不十分です

「propを持たないコンポーネントが多数ネストされているときにネストをフラットにしている記事」というのが正確ではないでしょうか?

(参考までに、実際に React.Context をフラットにした記事をあげておきます https://www.pandanoir.info/entry/2021/12/17/003300)

NiAkaNiAka

コメントおよび忠告をいただき感謝いたします。

たしかに、記事タイトルと内容に乖離があったように思うので、タイトルと誤解を招く表現を修正し、注意文を追加させていただきました