React.js/Next.jsで効率的に複数のProviderを管理する方法
はじめに
React.js / Next.jsでのプロジェクトを構築していると、複数のコンテキストプロバイダを使いたくなる場面に遭遇することがある(もしくは、実装した過去が残っている)かと思います。
e.g. 状態管理、認証、ナビゲーションなど
それぞれの役割を担うプロバイダが増えると、どんどんとネストが深くなっていく(ネスト波動拳)為、可読性や管理が複雑になり運用・保守に影響がでることもあるので、その解消法についての1つの提案になります。
(ネスト波動拳:画像はifでのネストの波動拳)
1.ネストが増えてしまう問題
通常、ReactではコンテキストAPIを使ってグローバルな状態を管理しますが、複数のプロバイダを使う場合、以下のようにコードがネストされてしまいます。
import { NavProvider } from '@/providers/NavContext';
import { AuthProvider } from '@/providers/AuthContext';
import { xxxxProvider } from '@/providers/xxxxContext';
export default function RootLayout({ children }) {
return (
<NavProvider>
<AuthProvider>
<xxxxProvider>
<Header />
<MainContent>{children}</MainContent>
<Footer />
<xxxxProvider>
</AuthProvider>
</NavProvider>
);
}
上記のように、コンテキストプロバイダが増えるとRootファイル(Next.jsを例に取っているのでここではRootLayout)次第に肥大化し、ネストも深くなりがちです。これを防ぎ、プロバイダの管理を一元化する為、BuildProviderTree
という関数を用意します。
2.BuildProviderTreeを実装する
実装する関数の内容としてはプロバイダの配列を受け取り、それを動的にラップする関数です。
これにより複数のプロバイダを一元的に管理し、コードの見通しを良くすることができます。
BuildProviderTreeの実装例
プロバイダを動的にラップし、渡されたchildrenをすべてのプロバイダで包む仕組みを提供します。
import React from 'react';
const BuildProviderTree = (providers: React.ReactElement, values = []) => {
return function ProviderTree(props) {
const lastIndex = providers.length - 1;
let children = props.children;
for (let i = lastIndex; i >= 0; i--) {
const element = providers[i];
const value = values[i];
if (value) {
children = React.cloneElement(element, { value }, children);
} else {
children = React.cloneElement(element, undefined, children);
}
}
return children;
};
};
export default BuildProviderTree;
providersの配列を受け取り、各プロバイダでchildrenをラップする処理を動的に行います。
3.ProviderTreeでプロバイダを管理する
BuildProviderTreeを使ってProviderTreeを実装し、複数のプロバイダを管理します。
この時、実装している各プロバイダを配列で一元管理します。
参考:NavProvider.ts
Toggleで開閉を管理する機能
'use client';
import { createContext, ReactNode, useCallback, useContext, useState } from 'react';
const NavContext = createContext({
navActive: false,
toggleHandler: () => {},
});
export const useNav = () => useContext(NavContext);
export const NavProvider = ({ children }: { children: ReactNode }) => {
const [navActive, setNavActive] = useState<boolean>(false);
const toggleHandler = useCallback(() => {
setNavActive((prev) => !prev);
}, []);
return (
<NavContext.Provider value={{ navActive, toggleHandler }}>
{children}
</NavContext.Provider>
);
};
import React from 'react';
import { NavProvider } from '@/providers/NavContext';
import BuildProviderTree from '@/providers/BuildProviderTree';
export const ProviderTree = ({ children }: { children: React.ReactNode }) => {
// 複数のプロバイダを配列で定義
const providers = [
<NavProvider />,
// 他のプロバイダもここに追加可能
];
// BuildProviderTreeを使用して動的にプロバイダツリーを構築
const Tree = BuildProviderTree(providers);
return <Tree>{children}</Tree>;
};
このProviderTreeコンポーネントでは実装したProvider(ここではNavProvider)をBuildProviderTreeに渡して、子コンポーネントをラップします。
これにより、ProviderTree内でのプロバイダのネストが不要となり、コードの見通しが非常に良くなります。
4.Rootファイルのシンプル化
最終的にRootファイル(例ではlayout.tsx)では、単に ProviderTreeを呼び出すだけでプロバイダが適切にラップされます。
import { ProviderTree } from '@/providers/ProviderTree';
export default function RootLayout({ children }) {
return (
<ProviderTree>
<Header />
<MainContent>{children}</MainContent>
<Footer />
</ProviderTree>
);
}
このように、Rootファイルでプロバイダの詳細を気にすることなく、ProviderTreeを呼び出してコンポーネントをラップできます。これにより、Rootファイルの肥大化を防ぎ、コードのシンプルさを保つことが可能です。
また、ドメイン毎にProviderTreeを作成すればglobalではなくlayoutやcomponentに応じた小さい単位での状態を管理することも容易になります。
結論
BuildProviderTreeを使うことで、複数のプロバイダを効率的に管理し、コードの見通しを良くすることができます。
特にプロバイダの数が多くなり管理が煩雑になっているorなりそうな既存の大規模なプロジェクトでは、少しのリファクタリングで柔軟にプロバイダを追加・削除できるこのアプローチは有効かと思います。
一方で、プロバイダが少ない場合や新規プロジェクトにおいて状態管理を別の方法で行う場合はそもそも不要なのでプロジェクトの規模や必要に応じて、最適なアプローチを取ることが良いと思います。
余談
新規開発を行う際にはプロジェクトの規模感にもよりますが個人的にはZustand(過去に状態管理法について書いた記事)やTanstack Queryのみでやりたいなと思っていたりしますが、すでにコンテキストAPIを使っているプロジェクトに参加したときに改修として有効かなと思い書きました。
Discussion