TL; DR
<Consumer>
{contextValue => /* contextValueを使ってReactNodeをレンダリングする */}
</Consumer>
ContextAPIに出てくるConsumerコンポーネントの内側で宣言する関数は、
- children prop
- render prop
を組み合わせて作られているもので、Consumerコンポーネントはイケイケなやつだった
今回は、ContextAPIの一部であるConsumerコンポーネントについて説明します。
一目ではややこしいので、 children prop/render prop を理解した上での腹落ちを目指します!
Context APIとは
React v16.3でアップデートされた Context API は、簡単に説明すると
- Providerに状態を注入すると、
- Consumerに状態が渡され、
- それを利用して画面が描画される
というものです。以下は公式のチュートリアルに載っているコードです
const ThemeContext = React.createContext('light'); // Contextを生成する
class App extends React.Component {
render() {
return (
{/* Providerに状態を注入するとConsumerに状態が渡される */}
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
const ThemedButton = (props) =>
<ThemeContext.Consumer>
{/* Consumerから渡される状態を引数に取って、画面を描画する */}
{theme => <Button {...props} theme={theme} />}
</ThemeContext.Consumer>
公式では、テーマカラーやログイン中ユーザーなど、状態が変わらないが様々なコンポーネントから参照される者に対して使うことが推奨されていますが、
状態を様々なコンポーネントに上手に渡すために使ってもいいと思っています。
Consumerのわかりづらい記法は、なんだ
上のサンプルを見てわかる通り、Consumerを使う際は以下のように記述します
<Consumer>
{contextValue => /* contextValueを使ってReactNodeをレンダリングする */}
</Consumer>
コード中の{contextValue => /* contextValueを使ってReactNodeをレンダリングする */}
👈この無名関数ちょっとよくわからないですよね、書かなくてもいいのかな?
... いえ、書かないと怒られてしまいます。
なぜなら、Consumerコンポーネントがpropsとして 「ReactNodeを返す関数」
を要求しているからです。
※ ReactNodeというのは文字列だったり、JSXエレメントだったり、booleanだったり。とりあえず表示できるものという理解で大丈夫です
試しに以下のようなコードを書くと怒られます。(ConsumerからcontextValueを受け取りたいのでこんなの全く意味ないですが...w)
<Consumer>
<p>{/* ReactNodeを返す関数を記述しないと怒られます */}</p>
</Consumer>
Consumerコンポーネントがpropsとして「ReactNodeを返す関数」を要求している と書きましたが、
普通はコンポーネントがpropsを受け取る時って、こんな感じでタグの中に記述していきますよね
<Component props1={} props2={} ... />
Consumerコンポーネントでは開始タグと閉じタグの間にpropsを記述していますが、
なぜこうなっているかというと、children prop
が使われているからです。
ContextAPIは脇においておき、children prop
について説明します
children propについて
参考URL
公式チュートリアル
react-in-patterns: Passing a child as a prop
children propはReact.ComponentのAPIで、React.Componentを継承したコンポーネントが利用できます。
何気なく使っているタグも、children propを使っています。
<div>
<p>{/* <div>はこれらを */}</p>
<span>{/* children propとして受け取っています */}</p>
<div>
children propを受け取るコンポーネント側の内部がどうなっているか知るために、適当なコンポーネントを作ってみます
import * as React from "react"
class App extends React.Component {
render() {
return(
<div>
{/* this.orops.childrenに、Appタグに挟まれたコンポーネントが渡ってくる */}
{this.props.children}
</div>
)
}
}
<App>
<p>{/* children propとなるやつ */}</p>
</App>
Appコンポーネントタグに挟まれたコンポーネントが this.props.children
として利用できるのがわかりますね。
さて、children propについて理解した上でもう一度 Consumerコンポーネントの利用例を見てみます
<Consumer>
{contextValue => /* contextValueを使ってReactNodeをレンダリングする */}
</Consumer>
...あら、Consumerのタグに挟まれた要素はchildren propsとして渡されることはわかりますが、
コンポーネントではなく、関数の形になっていますね。
ここで、children propの次に理解する必要があるのが、 render prop
です。
(ContextAPIについては再度置いておきます)
render propについて
参考URL
公式チュートリアル
react-in-patterns: Function as a children, render prop
render propは簡単に言うと 「コンポーネントのrenderメソッドを外部から定義するためのテクニック」です。
色々な使い方があるので「こういうケースで使うべき!」とは言えないですが、
個人的には以下の状況が重なった時に便利そうだと思います(とはいえ実戦で使ったことないです)
- 状態と画面描画の分離をしたいとき
- 上で言った分離を1つのパターンとして共通利用したいとき
例えば、ローディング時のViewとローディング完了時のViewを出し分けるというのが様々な画面で重複していた場合、
render propで以下のように書くことができます (Typescriptで書いています)
interface LoadingProps {
url: string,
render: (isLoading: boolean) => JSX.Element
}
interface LoadingStates {
isLoading: boolean
data: any[]
}
class LoadingState extends React.Component<Props, States> {
state: States = {
isLoading: false,
data: null
};
fetch = () => {
// ... this.props.urlを使ってapiからデータ取得する処理を記述
this.setState({
isLoading: false,
data: /* 取得したデータ */
});
}
componentDidMount() {
// ... 通信処理の実行
this.setState({isLoading: true}, this.fetch);
}
render() {
// ... render propに状態を渡すだけで、描画については関与しない
return this.props.render(...this.state);
}
}
// apiのurlと、通信していない時に表示したいコンポーネントを引数に渡している
const WithLoadingState = ({url, component}: {url: string, Component: JSX.Element}) =>
<LoadingState
url
render={state: LoadingStates) =>
state.isLoading
? <p>Loading...</p>
: <Component data={state.data} />
} />;
上のコードでは、
- 状態については全て
LoadingState
コンポーネントにお任せして、 - 何をどう表示するかに関しては
WithLoadingState
コンポーネントにお任せする
という「分離」を表現しています
本題: Consumerタグに挟まれる関数の正体とは
render prop は、通例で render という名前にしていますが、名前はなんでも構いません。
名前は何でもいいどころか、実は名前をつけない方法もあります。
それがまさに、children propを render propとして使う方法です。
そしてそれがConsumerコンポーネントの関数の正体です。
<Consumer>
{contextValue => /* contextValueを使ってReactNodeをレンダリングする */}
</Consumer>
このコードは、
Consumerコンポーネントが、 「コンポーネントを返す関数(render prop)」
を 「children propとして」
要求している
という意味でした。
先ほどのrender propの例でお見せした 状態と画面描画の分離 をConsumerコンポーネントに当てはめると、
- Consumerコンポーネントが状態を持ち、
- その状態をもとに何を描画するかはchildren prop(関数)に任せる
と言えそうです。
最後に、ContextAPIの型定義を見ると、Provider側 で 受け取る状態(value: T) をConsumer側のchildren propで利用しているのがわかります。
interface ProviderProps<T> {
value: T;
children?: ReactNode;
}
interface ConsumerProps<T> {
children: (value: T) => ReactNode;
unstable_observedBits?: number;
}
なので、Consumerタグに挟まれるのはコンポーネントではなく、
状態を受け取りReactNodeを返す関数
でないといけないということになります。
なんと、ここまで説明に時間かかりましたが、
結論、
Consumerはイケイケなコンポーネントですね!!!
さいごに
記事中で紹介したrender propは比較的新しいテクニックで、日本語の解説を見つけるのも難しいです。
なにか理解の進む記事があれば、ぜひご紹介ください!