はじめに
React のコードでは、Provider をネストすることがよくあると思いますが、そのネストを回避する方法を提案する記事です。
以下のような、プロバイダのネストを良く思っていない人向けの記事です。
また、この記事を読むと部分適用と関数合成という概念を学習できます。
import React from 'react'
// UI
import { ThemeProvider } from '@material-ui/core/styles'
import theme from 'src/app/styles/theme'
// 状態管理
import { Provider as ReduxProvider } from 'react-redux'
import { store } from 'src/app/redux/store'
// web API
import { Provider as UrqlProvider } from 'urql'
import { client } from 'src/app/graphql/urql'
// util
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
// 他の Providers (dummy)
declare const FooProvider: React.FC // 内部に Provider を含む関数コンポーネントのダミー
declare const BarProviderDependingOnFoo: React.FC // FooProvider に依存するコンポーネント
// メインのコンポーネントとその props (dummy)
declare const Component: React.FC
declare const pageProps: object
const App: React.FC = () => {
return (
<ThemeProvider theme={theme}>
<ReduxProvider store={store}>
<UrqlProvider value={client}>
<DndProvider backend={HTML5Backend}>
<FooProvider>
<BarProviderDependingOnFoo>
<Component {...pageProps} />
</BarProviderDependingOnFoo>
</FooProvider>
</DndProvider>
</UrqlProvider>
</ReduxProvider>
</ThemeProvider>
)
}
export default App
※ FooProvider
は 例えば constate で作成した Provider などです。
Provider をまとめる
Provider は以下のようにまとめる(合成する)ことができます。
// ...前略
import { pipeComponents, partial } from 'src/utils/pipeComponents' // 追加
// ...中略
const Provider = pipeComponents(
partial(ThemeProvider, { theme }),
partial(ReduxProvider, { store }),
partial(UrqlProvider, { value: client }),
partial(DndProvider, { backend: HTML5Backend }),
FooProvider,
BarProviderDependingOnFoo
)
const App: React.FC = () => {
return (
<Provider>
<Component {...pageProps} />
</Provider>
)
}
export default App
pipeComponents
の引数としてより先頭側に与えたコンポーネントが、より上流のコンポーネントとなります。
pipeComponents
と partial
の実装は以下の通りです。
import React from 'react'
type Component = React.ComponentType // children のみを prop として持つコンポーネント
type AtLeast2 = [Component, Component, ...Component[]] // 型で引数の数を2つ以上に限定
// コンポーネント用部分適用関数
export const partial = <P extends object>(
Component: React.ComponentType<P>,
props: P
): Component => ({ children }) => React.createElement(Component, props, [children])
// コンポーネント用合成関数
export const pipeComponents = (...components: AtLeast2): Component => {
return components.reduce((Acc, Cur) => {
return ({ children }) => (
<Acc>
<Cur>{children}</Cur>
</Acc>
)
})
}
実装についてピンとこない方は以下の「補足:部分適用と関数合成」で実装を解説しています。
メリット
- 可読性(少しだけ読みやすくなる。)
- 保守性(プロバイダの順番を入れ替えたり、プロバイダを追加したりするのが少し楽になる。変更時の差分が少し分かりやすくなる。プロバイダを Provider.tsx などようにまとめておく場合に少し楽。)
- 見た目(好みですが、縦に整列していて良いと思います。コーディングも少し楽になります。)
プロバイダ(というよりもコンテキスト)は適切な粒度で作った方がパフォーマンス上良い1ので、プロバイダをたくさん作ること自体は悪くないと思っています。個人的にはプロバイダを増やすことに対する抵抗感が減りました。
補足:部分適用と関数合成
部分適用
部分適用は関数のパラメータの一部を固定して、新しい関数を作る処理です。
下記例では、掛け算をする関数 multiply
の引数を 2
で固定して新しい関数 double
を作成しています。
部分適用をしてくれる関数として ramda ライブラリを使います。
import { partial } from 'ramda'
const multiply = (a, b) => a * b
const double = partial(multiply2, [2])
double(2) //=> 4
「Provider をまとめる」に記載した partial
は、children
prop 以外の props を固定したコンポーネントを作成します。言い換えると、children のみ を引数として取るコンポーネントを作成します。
関数合成
関数合成はざっくりと説明すると複数の関数から一つの関数を作る処理です。
下記例では二つの関数を合成する関数(pipe2
)を使って、double
と increment
を合成し、doubleAndInc
という新しい関数を作成しています。
const pipe2 = (fn1, fn2) => (num) => fn2(fn1(num))
const double = (num) => num * 2
const increment = (num) => num + 1
const doubleAndInc = pipe2(double, increment)
doubleAndInc(1) // 3
コンポーネントは、children 等 の props を引数に取り、UI(ReactElement)を出力する関数と考えられます。
「Provider をまとめる」に記載した pipeComponents
では、children のみ を引数として取る複数の「関数」を合成しています。
部分適用と関数合成を組み合わせて使う
関数の形(シグネチャ)が異なる関数でも、部分適用によって関数の形を統一させることにより、合成することができるようになります。
import { partial } from 'ramda'
const pipe2 = (fn1, fn2) => (num) => fn2(fn1(num))
const multiply = (a, b) => a * b
const increment = (num) => num + 1
// ↓ 部分適用なしだと pipe2 で合成できない
const doubleAndInc_ng = (num) => multiply(increment(num), /* ❗ 引数が不足 */)
// ↓ 部分適用ありだと pipe2 で合成できる
const double = partial(multiply, [2])
const doubleAndInc_ok = (num) => double(increment(num))
// ↓ 部分適用と関数合成を組み合わせて使う
const doubleAndInc = pipe2(
partial(multiply,[2]),
increment
)
doubleAndInc(1) // 3
「Provider をまとめる」に記載した app.tsx でも部分適用partial
と関数合成pipeComponents
を組み合わせています。
参考