56
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【React - ContextAPI】Consumerの正体は、イケイケなコンポーネントだった

Last updated at Posted at 2018-06-06

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は比較的新しいテクニックで、日本語の解説を見つけるのも難しいです。

なにか理解の進む記事があれば、ぜひご紹介ください!

56
41
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
56
41

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?