provider-pattern について
コンポーネントツリーでデータを共有する React の Provider Pattern を解説するスキルです。Context、フック、Provider コンポーネントの使い方と、再レンダーなどの注意点を扱います。
主な機能
- 01React Context による値の共有
- 02useContext による値の読み取り
- 03カスタムフックでアクセスを集約
- 04関心ごとに Provider を分割
- 05再レンダーの影響を考慮
活用シーン
- 01コンポーネント間でテーマを共有
- 02認証やロケールの値を提供
- 03複数階層の props 受け渡しを削減
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ patternsdev/ skills --skill provider-patternインストール後、Claude Code で/provider-patternを入力して読み込まれたか確認します。
詳細
Providerパターン
目次
アプリケーション内の多くのコンポーネント(場合によってはすべて)でデータを利用できるようにしたいことがあります。propsを使ってコンポーネントにデータを渡すことはできますが、アプリケーション内のほぼすべてのコンポーネントがpropsの値にアクセスする必要がある場合、それを行うのは難しくなります。
その結果、コンポーネントツリーの深い階層までpropsを渡す、いわゆる_プロップドリリング_がよく発生します。propsに依存するコードのリファクタリングがほぼ不可能になり、特定のデータの出どころを把握することも難しくなります。
使用する場面
- 多くのコンポーネントが同じデータ(テーマ、認証、ロケール)にアクセスする必要がある場合に使用します
- 複数のコンポーネント階層にわたるプロップドリリングが扱いにくくなった場合に役立ちます
手順
React.createContext()でContextを作成し、そのProviderでコンポーネントをラップします- 値を利用するコンポーネントで
useContextフックを使い、提供された値にアクセスします - カスタムフック(例:
useThemeContext)を作成して、Contextを利用するロジックをまとめます - 値が頻繁に更新される場合は、Contextの使いすぎを避けてください。変更時にすべての利用側コンポーネントが再レンダリングされます
- 不必要な再レンダリングを最小限に抑えるため、関心事ごとにContextを分割します
詳細
App コンポーネントに特定のデータが含まれているとします。コンポーネントツリーのずっと下に、同じデータを必要とする ListItem、Header、Text コンポーネントがあります。これらのコンポーネントにデータを渡すには、複数の階層のコンポーネントを経由して渡す必要があります。
コードベースでは、次のようになります。
function App() {
const data = { ... }
return (
<div>
<SideBar data={data} />
<Content data={data} />
</div>
)
}
const SideBar = ({ data }) => <List data={data} />
const List = ({ data }) => <ListItem data={data} />
const ListItem = ({ data }) => <span>{data.listItem}</span>
const Content = ({ data }) => (
<div>
<Header data={data} />
<Block data={data} />
</div>
)
const Header = ({ data }) => <div>{data.title}</div>
const Block = ({ data }) => <Text data={data} />
const Text = ({ data }) => <h1>{data.text}</h1>
このように props を渡していくと、かなり煩雑になることがあります。将来 data prop の名前を変更する場合、すべてのコンポーネントで名前を変更する必要があります。アプリケーションが大きくなるほど、prop のバケツリレーは難しくなります。
このデータを使う必要のないコンポーネントの階層をスキップできると理想的です。prop のバケツリレーに頼らず、data の値にアクセスする必要があるコンポーネントが、その値に直接アクセスできる仕組みが必要です。
そこで役立つのが プロバイダーパターン です。プロバイダーパターンを使うと、複数のコンポーネントでデータを利用できるようになります。各階層で props を使ってデータを渡すのではなく、すべてのコンポーネントを Provider でラップできます。Provider は、Context オブジェクトが提供する高階コンポーネントです。React が提供する createContext メソッドを使って、Context オブジェクトを作成できます。
Provider は value prop を受け取り、渡したいデータを格納します。この Provider でラップされた すべての コンポーネントは、value prop の値にアクセスできます。
const DataContext = React.createContext()
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
各コンポーネントに data prop を手動で渡す必要はなくなりました。各コンポーネントは useContext フックを使って data にアクセスできます。このフックには、data が参照している Context を渡します。この場合は DataContext です。useContext フックを使うと、Context オブジェクトのデータを読み書きできます。
const DataContext = React.createContext();
function App() {
const data = { ... }
return (
<div>
<DataContext.Provider value={data}>
<SideBar />
<Content />
</DataContext.Provider>
</div>
)
}
const SideBar = () => <List />
const List = () => <ListItem />
const Content = () => <div><Header /><Block /></div>
function ListItem() {
const { data } = React.useContext(DataContext);
return <span>{data.listItem}</span>;
}
function Text() {
const { data } = React.useContext(DataContext);
return <h1>{data.text}</h1>;
}
function Header() {
const { data } = React.useContext(DataContext);
return <div>{data.title}</div>;
}
data の値を使わないコンポーネントは、data を扱う必要がありません。prop の値を必要としないコンポーネントをいくつもの階層にわたって経由しながら props を渡す必要がなくなるため、リファクタリングがずっと簡単になります。
プロバイダーパターンは、グローバルデータを共有するのにとても便利です。よくある使い方は、UI のテーマ状態を複数のコンポーネントで共有することです。
リストを表示するシンプルなアプリがあるとします。ユーザーがスイッチを切り替えて、ライトモードとダークモードを切り替えられるようにしたいとします。ユーザーがダークモードからライトモードに、またはその逆に切り替えると、背景色とテキストの色も変わる必要があります。現在のテーマの値を各コンポーネントに渡す代わりに、コンポーネントを ThemeProvider でラップし、現在のテーマの色を Provider に渡せます。
export const ThemeContext = React.createContext();
const themes = {
light: {
background: "#fff",
color: "#000",
},
dark: {
background: "#171717",
color: "#fff",
},
};
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<div className={`App theme-${theme}`}>
<ThemeContext.Provider value={providerValue}>
<Toggle />
<List />
</ThemeContext.Provider>
</div>
);
}
Toggle コンポーネントと List コンポーネントはどちらも ThemeContext Provider でラップされているため、Provider に value として渡された theme と toggleTheme の値にアクセスできます。
Toggle コンポーネント内では、toggleTheme 関数を使ってテーマを適切に更新できます。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function Toggle() {
const theme = useContext(ThemeContext);
return (
<label className="switch">
<input type="checkbox" onClick={theme.toggleTheme} />
<span className="slider round" />
</label>
);
}
List コンポーネント自体は、現在のテーマの値を気にする必要はありません。しかし、ListItem コンポーネントは必要とします。ListItem 内で theme Context を直接使えます。
import React, { useContext } from "react";
import { ThemeContext } from "./App";
export default function TextBox() {
const theme = useContext(ThemeContext);
return <li style={theme.theme}>...</li>;
}
これで、現在のテーマの値を必要としないコンポーネントにデータを渡す必要はありません。
フック
コンポーネントにコンテキストを提供するフックを作成できます。各コンポーネントで useContext と Context をインポートする代わりに、必要なコンテキストを返すフックを使用できます。
function useThemeContext() {
const theme = useContext(ThemeContext);
return theme;
}
有効なテーマであることを確認するため、useContext(ThemeContext) が falsy な値を返した場合はエラーをスローします。
function useThemeContext() {
const theme = useContext(ThemeContext);
if (!theme) {
throw new Error("useThemeContext must be used within ThemeProvider");
}
return theme;
}
コンポーネントを ThemeContext.Provider コンポーネントで直接ラップする代わりに、専用のプロバイダーコンポーネントを切り出せます。これにより、コンテキストのロジックをレンダリングコンポーネントから分離し、再利用性を高められます。
function ThemeProvider({ children }) {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
const providerValue = {
theme: themes[theme],
toggleTheme,
};
return (
<ThemeContext.Provider value={providerValue}>
{children}
</ThemeContext.Provider>
);
}
export default function App() {
return (
<ThemeProvider>
<div className="App">
<Toggle />
<List />
</div>
</ThemeProvider>
);
}
ThemeContext にアクセスする必要がある各コンポーネントは、useThemeContext フックを使うだけで済みます。
export default function TextBox() {
const theme = useThemeContext();
return <li style={theme.theme}>...</li>;
}
異なるコンテキストごとにフックを作成することで、プロバイダーのロジックとデータをレンダリングするコンポーネントを簡単に分離できます。
ケーススタディ
ライブラリによっては、利用側のコンポーネントで値を使用できる組み込みプロバイダーが用意されています。その好例が styled-components です。
styled-components ライブラリは ThemeProvider を提供しています。各 styled component は、このプロバイダーの値にアクセスできます!自分でコンテキスト API を作成する代わりに、用意されているものを使用できます!
import { ThemeProvider } from "styled-components";
export default function App() {
const [theme, setTheme] = useState("dark");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return (
<div className={`App theme-${theme}`}>
<ThemeProvider theme={themes[theme]}>
<Toggle toggleTheme={toggleTheme} />
<List />
</ThemeProvider>
</div>
);
}
ListItem コンポーネントにインラインの style プロパティを渡す代わりに、styled.li コンポーネントにします。styled component なので、theme の値にアクセスできます!
import styled from "styled-components";
export default function ListItem() {
return (
<Li>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</Li>
);
}
const Li = styled.li`
${({ theme }) => `
background-color: ${theme.backgroundColor};
color: ${theme.color};
`}
`;
ThemeProvider を使えば、すべての styled component に簡単にスタイルを適用できます!
トレードオフ
メリット
Provider パターン/Context API を使うと、各コンポーネント層を手動で経由させることなく、多くのコンポーネントにデータを渡せます。
コードのリファクタリング時に、誤ってバグを持ち込むリスクを減らせます。以前は、後からプロパティ名を変更したい場合、この値が使われているアプリケーション全体でそのプロパティ名を変更する必要がありました。
アンチパターンと見なされることもある prop-drilling に対処する必要がなくなります。以前は、特定のプロパティ値がどこで発生したのかが常に明確ではなかったため、アプリケーションのデータフローを理解するのが難しい場合がありました。Provider パターンを使えば、そのデータを必要としないコンポーネントにプロパティを不必要に渡す必要はありません。
Provider パターンを使うと、コンポーネントにグローバル状態へのアクセスを提供できるため、何らかのグローバル状態を簡単に保持できます。
デメリット
場合によっては、Provider パターンを使いすぎるとパフォーマンスの問題につながることがあります。コンテキストを 利用する すべてのコンポーネントは、状態が変化するたびに再レンダリングされます。
例を見てみましょう。Button コンポーネントの Increment ボタンをクリックするたびに値が増える、シンプルなカウンターがあります。また、Reset コンポーネントにはカウントを 0 に戻す Reset ボタンもあります。
Reset コンポーネントも useCountContext を利用しているため、再レンダリングされました。小規模なアプリケーションでは、これはそれほど問題になりません。大規模なアプリケーションでは、頻繁に更新される値を多くのコンポーネントに渡すと、パフォーマンスに悪影響を及ぼす可能性があります。
不要な値を含む、更新される可能性のあるプロバイダーをコンポーネントが利用しないようにするには、用途ごとに複数のプロバイダーを作成できます。