React.js Advent Calendar 2018の5日目の記事です。
こんにちは@mrsekutです。
先日、React Hooksが出てきてそれについて記事も書いたんですが、公式のHooksの中にuseContextというものがありました。
これはContextAPIのHooksですが、そもそもContextAPIがどういうものかを知らなかったのでこの機会に調べてみることにしました。
githubはこちらです。
tl;dr
- ContextAPIとはpropsにグローバルにアクセスできる機能である。
- hooksを使ってContextAPIを使うとより簡潔に書けるようになる。
- 先日のreact-reduxのリリースでContextAPIが追加されたので、その使い方を見てみる。
最小プロジェクトの環境構築
手を動かして試してもらえるように簡単な環境構築の例を示しておきます。
とりあえずContextAPIの概要が知りたいだけ!という方はここは読み飛ばしてください。
create-react-appを用いて作ります。
プロジェクト名は「trial-react-context」とでもします。
$ mkdir trial-react-context
$ cd trial-react-context
create-react-appをインストールします。$ npx create-react-app trial-react-context
$ cd trial-react-context
hooksを使うために、v16.7.0-alphaのReactを入れます。$ yarn add -D react@16.7.0-alpha.0 react-dom@16.7.0-alpha.0
linterもすでに用意されているのでこれも入れてしまいましょう。$ yarn add -D eslint-plugin-react-hooks@next
linterを適用させるために、eslintrc.jsを作ります。
1 2 3 4 5 6 7 8 9 |
// .eslintrc.js 'use strict'; module.exports = { plugins: ['react', 'react-internal', 'react-hooks'], rules: { 'react-hooks/rules-of-hooks': 'error' } }; |
とりあえずはこれで環境が整いました。$ yarn start
で立ち上がります。
これからContextAPIについて見ていきましょう。
従来のReactでのpropsのバケツリレーの例
github: ConventionalPattern/index.jsx
まずはContextAPIを用いない場合の、propsのバケツリレーの例を見てみます。
親コンポーネントから子コンポーネントへデータを渡したいときは以下のように書きますね。
1 2 3 4 5 6 7 8 9 10 |
// 親 export default class Parent extends React.Componet { render() { const count = 1; return <Child count={count} />; // `count`を子孫にわたす } } // 子 const Child = ({ count }) => <div>count: {count}</div>; |
上の例ではまだ親と子しかいないため、そこまで複雑にはなりません。
次に、親、子、孫、曾孫と続いてpropsを渡していく例を見てみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 親 データを子に渡す export default class Parent extends React.Component { render() { const count = 1; const word = 'hello'; return <Child count={count} word={word} />; } } // 子 ここでは使わないけど渡さないといけない const Child = ({ count, word }) => <Grandson count={count} word={word} />; // 孫 ここでも使わないけど渡さないといけない const Grandson = ({ count, word }) => <GreatGrandchild count={count} word={word} />; // 曾孫 やっとここで使いたい const GreatGrandchild = ({ count, word }) => ( <> <div>count: {count}</div> <div>word: {word}</div> </> ); |
親で作られたデータを曾孫の中で使いたいのですが、そのためには子、孫にもバケツリレーしてもらう必要があります。
このようにpropsのバケツリレーが続いてしまうことを「Props Driling問題」と言います。
今回の例では、渡すデータはcountとwordの2つだけでしたが、これがもっと増えたり、曾孫より子孫が増えたりすると、記述量が増えてしまいます。
従来の解決方法
こんな感じでpropsを伝達するのは煩わしいので、今までもいくつかの対策がありました。
spred attributesを使う
...this.props
を使って、親から受け取った全propsを子孫に流してしまう方法です。
記述量は減りますが、毎回書かなければならないので本質的な解決にはなっていません。
render propsを使う
上のコードをrender props
を使って書き直したものが以下です。
親の中に曾孫を書いてしまうことで記述量を減らしています。
ですが、この方法だと再利用しにくそうです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import * as React from 'react'; // 親 export default class Parent extends React.Component { render() { const count = 1; const word = 'hello'; // 曾孫をここに書いてしまう!! const greatGrandchild = ( <> <div>count: {count}</div> <div>word: {word}</div> </> ); return <Child ggc={greatGrandchild} />; } } // 子 const Child = ({ ggc }) => <Grandson ggc={ggc} />; // 孫 const Grandson = ({ ggc }) => ggc; |
Reduxを使う
それで結局Reduxという外部のライブラリに頼ることになっていました。
Reduxを使うことで、あらゆるstateに対してpropsとしてglobalにアクセスすることができていました。
ContextAPIを見ていく
公式: Context – React
github: SimpleContextAPI/index.jsx
これを外部ライブラリに頼らずとも解決する方法としてReact製のContextという機能が出てきました。
これを使うことで、親から曾孫へpropsをワープして使用することができます。
用いた最小の例
上述のコードをContext APIを用いて書き直してみました。Provider
とConsumer
という謎のコンポーネントを使っているのと、子、孫でpropsを伝搬させていない点が異なります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import React, { createContext } from 'react'; const { Provider, Consumer } = createContext(); // 親 const Parent = () => ( <Provider value={{ count: 1, word: 'hello' }}> <Child /> </Provider> ); export default Parent; // 子 propsを伝搬させてない const Child = () => <Grandson />; // 孫 同じく伝搬させてない const Grandson = () => <GreatGrandchild />; // 曾孫 const GreatGrandchild = () => ( <Consumer> {({ count, word }) => ( <> <div>values: {count}</div> <div>word: {word}</div> </> )} </Consumer> ); |
上のコードではChildとGrandsonは何もしていないので、書く必要はないのですが、「バケツリレーしなくてもいいよ感」を出すためにわざわざ書いています。
APIの簡単な解説
React.createContext
この関数を実行すると、後述するProviderとConsumerをペアで返します。
上記のコードのように2値を一気に受け取る方法もありますが、下のように名前をつける方法もあります。
複数のContextを利用する場合はこっちの方が良いかもしれません。
1 2 3 4 5 |
const Root = React.createContext; // `Root`という名前を付けて受け取る ... <Root.Provider /> // `Root`のProvider ... <Root.Consumer /> // `Root`のConsumer |
このAPIは引数にテスト時などに使うdefault valueを取ります。
「初期値」ではないことに注意が必要です。
Provider
Consumerにcontextを渡す側のコンポーネントです。value
を使ってデータを渡すことができます。
上の例のような値だけでなく、関数も渡すことができます。
Reduxが用意しているProviderコンポーネントと同じような役割で、全く同じ名前なのは…🤬🤬🤬
Consumer
Providerからcontextを受け取ります。
ちょっと変わったことに中にはjsxではなく関数を書きます。
1 2 3 |
<Consumer> {(contextを受け取り) => { ReactNodeを返したり }} </Consumer>; |
この、ちょっと特殊な使い方をするConsumerの内部については以下の記事で解説されています。
【参考】【React – ContextAPI】Consumerの正体は、イケイケなコンポーネントだった – Qiita
ちなみに
公式のDocumentの例では、「テーマカラー」や「ログイン中ユーザー」など、「状態が頻繁に変わるわけではないが、複数のコンポーネントからアクセスされるデータ」をContextとして扱っています。
ですが、これに限らず使えると思います。
少し拡張したコードを書いてみる
次の例ではまだ紹介していないAPIであるClass.contextType
を使ってみるのと、値の他に関数も渡した例を書いてみます。
github: SlightlyExtendedContextAPI/index.jsx
よくある「ボタンを押せばインクリメントするやつ」です。
stateを定義し、関数もcontextとしてConsumerに渡しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
import React from 'react'; // 名前を付けてcontextを作る const CounterContext = React.createContext(); // 親 export default class Parent extends React.Component { constructor(props) { super(props); this.state = { count: 0 // stateを定義 }; } increment = () => { this.setState({ count: this.state.count + 1 }); }; render() { return ( <CounterContext.Provider value={{ count: this.state.count, increment: this.increment }} // 関数も渡す > <Child /> </CounterContext.Provider> ); } } // 子 何もしてない const Child = () => <Grandson />; // 孫 これはclass class Grandson extends React.Component { static contextType = CounterContext; // contextTypeを使う render() { const { count, increment } = { ...this.context }; // contextを受け取る return ( <> <p>count: {count}</p> <button onClick={increment}>increment</button> </> ); } } |
動かしたイメージです。
APIの簡単な解説
class.contextType
孫コンポーネントでは、static contextType = CounterContext;
の部分でcontextを受け取っています。
contextTypeを使うことで、Consumerの中でcontextにthis.context.hoge
の形でアクセスできるようになります。
これによりlifecycleや他のmethodの中でもcontextを使うことができます。
少し感じるContextAPIの問題点
これは完全に、後述するhooksのための伏線ですが、ContextAPIを利用したコードを書くときの問題点をいくつか挙げます。
Consumerの中が関数ってわかりにくくない?
できるだけclassって書きたくなくない?
これを解消するのがこれから説明するuseContextです。
参考
HooksのuseContextを使う
公式: Hooks API Reference – React
github: TrialContextAPI/index.jsx
hooksを使ってさっきの拡張したやつを書き直してみます。
useContext以外にちゃっかりuseStateも使っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import React, { createContext, useState, useContext } from 'react'; // contextを作る const CounterContext = createContext(); // 親 const Parent = () => { const [num, setNum] = useState(0); return ( <CounterContext.Provider value={{ num, increment: () => setNum(num + 1) }} > <Child /> </CounterContext.Provider> ); }; export default Parent; // 子 const Child = () => <Grandson />; // 孫 const Grandson = () => { const counter = useContext(CounterContext); // hooksを使う return ( // いつも通りの書き方ができる <> <p>{counter.num}</p> <button onClick={counter.increment}>increment</button> </> ); }; |
さっきの例に比べてだいぶ簡素に書けました。
親もclassを使わずに関数コンポーネントで書けます。
useStateに関しては前に書いたこの記事などを参考にしてみてください。
ここでは、useContextを見てみます。
ちょっとした解説
useContextの引数部分には、createContextで作ったContextの名前を設定します。
これだけで、contextにアクセスできるようになります。
孫コンポーネントを再掲します。
1 2 3 4 5 6 7 8 9 10 11 |
// 孫 const Grandson = () => { const counter = useContext(CounterContext); // hooksを使う return ( // いつも通りの書き方ができる <> <p>{counter.num}</p> <button onClick={counter.increment}>increment</button> </> ); }; |
今までは孫はConsumerコンポーネントを使って、中に関数を書くという奇抜な書き方をしないといけませんでしたが、useContextを使うことで、Consumerも関数も書かず従来どおりのjsxを記述することができます。
【参考】:How the useContext Hook Works
ContextAPI + react-redux
話は変わりまして、react-reduxの方でも少し前にβ版がリリースされました。
【参考】Release v6.0.0-beta.1 · reduxjs/react-redux
せっかくなので、これも試してみましょう。
このβ版のreact-reduxとReduxをインストールします。
$ yarn add react-redux@next
$ yarn add redux
それでは簡単なコードを書いてみます。
小さいですがReduxを書く必要があるので、action、reducer、storeを追加することになりますが、このへんはあまり本質じゃないので、ここにはコードは掲載しません。
githubを参考にしてみてください。
ducksパターンで書いてあります。
modules: modules/index.jsx
store: store.jsx
以下のコードはReduxのrootとなる部分です。
普段と同じようにreact-reduxのProviderコンポーネントを使ってreduxと接続します。
1 2 3 4 5 |
const App = () => ( <Provider store={store}> <TrialReactReduxContext /> </Provider> ); |
肝は子供側です。
react-reduxからReactReduxContext
をimportして使います。
本来の書き方だとconnectしたりする必要がありましたが、その辺を書かずにstateにアクセスできるようになりました。
1 2 3 4 5 6 7 8 9 10 11 |
import React from 'react'; import { ReactReduxContext } from 'react-redux'; // connectせずにstateの`num`にアクセス const TrialReactReduxContext = () => ( <ReactReduxContext.Consumer> {({ storeState }) => <>num: {storeState.reducer.num}</>} </ReactReduxContext.Consumer> ); export default TrialReactReduxContext; |
Consumerの中のstoreState
ってなんだ??って感じですが、これはProviderコンポーネントの内部コードを読めばわかります。
以下のコードはProviderコンポーネントの内部コードの一部です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Provider extends Component { constructor(props) { super(props) const { store } = props this.state = { storeState: store.getState(), // storeStateを定義 store } } // 省略 render() { const Context = this.props.context || ReactReduxContext return ( // ContextAPIを使っている! <Context.Provider value={this.state}> // stateを渡している {this.props.children} </Context.Provider> ) } |
このバージョンからProviderコンポーネントの定義にContextAPIが使われるようになりました。
Providerコンポーネントの中のstateにstoreState
を定義し、render()の中でContext.Provider
を返しているのがわかります。
これによってReactReduxContextコンポーネントを使えば、どこからでもstateにアクセスすることができますね。
注意なのは、actionにはアクセス出来ないということです。
actionを使うためには今まで通りconnectする必要があります。
今回の例でも、modulesの中でincrementするactionを定義していますが、これは使っていません。
ちなみに、さきほどのuseContextを使って書き直せば以下のようになります。
若干、簡潔になり普段どおりのjsxで書けますが、contextの入れ子が1段深まります。
どっちが良いかは好み分かれそうですね。
1 2 3 4 5 6 7 8 9 |
import React, { useContext } from 'react'; import { ReactReduxContext } from 'react-redux'; const TrialReactReduxContext = () => { const context = useContext(ReactReduxContext); return <div>num: {context.storeState.reducer.num}</div>; }; export default TrialReactReduxContext; |
で、結局いつ使うのか?
長々書いてきましたが、結局ContextAPIはいつ使えばいいのでしょうか
ここまでいろいろ書いた上で言うのもあれですが、結局は今後もReduxを使っていくことになる気がします。
ですが、Reduxを使いたくないくらいの小規模のプロジェクトを作るならContextAPIが重宝するかもしれません。
また、Reduxを使う場合でも上の例のようにReactReduxContextを使い、部分的にContextAPIを使うことで便利に書けたりできそうです。
ここに関してはReduxがどうのこうのというよりreact-reduxが今後どう対応していくかにかかっている気がします。
が、Reactさん的にはReduxを使わずとも便利に扱える方向にしていきたいのでしょうか。
まだ、いずれ大きな変更があるかもしれません。