contextAPIとuseContextを知る、それとreact-reduxも。

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を作ります。

とりあえずはこれで環境が整いました。
$ yarn startで立ち上がります。

これからContextAPIについて見ていきましょう。

従来のReactでのpropsのバケツリレーの例

github: ConventionalPattern/index.jsx

まずはContextAPIを用いない場合の、propsのバケツリレーの例を見てみます。
親コンポーネントから子コンポーネントへデータを渡したいときは以下のように書きますね。

上の例ではまだ親と子しかいないため、そこまで複雑にはなりません。
次に、親、子、孫、曾孫と続いてpropsを渡していく例を見てみます。

親で作られたデータを曾孫の中で使いたいのですが、そのためには子、孫にもバケツリレーしてもらう必要があります。

このようにpropsのバケツリレーが続いてしまうことを「Props Driling問題」と言います。

今回の例では、渡すデータはcountとwordの2つだけでしたが、これがもっと増えたり、曾孫より子孫が増えたりすると、記述量が増えてしまいます。

従来の解決方法

こんな感じでpropsを伝達するのは煩わしいので、今までもいくつかの対策がありました。

spred attributesを使う

...this.propsを使って、親から受け取った全propsを子孫に流してしまう方法です。
記述量は減りますが、毎回書かなければならないので本質的な解決にはなっていません。

render propsを使う

上のコードをrender propsを使って書き直したものが以下です。
親の中に曾孫を書いてしまうことで記述量を減らしています。
ですが、この方法だと再利用しにくそうです。

Reduxを使う

それで結局Reduxという外部のライブラリに頼ることになっていました。
Reduxを使うことで、あらゆるstateに対してpropsとしてglobalにアクセスすることができていました。

ContextAPIを見ていく

公式: Context – React
github: SimpleContextAPI/index.jsx

これを外部ライブラリに頼らずとも解決する方法としてReact製のContextという機能が出てきました。
これを使うことで、親から曾孫へpropsをワープして使用することができます。

用いた最小の例

上述のコードをContext APIを用いて書き直してみました。
ProviderConsumerという謎のコンポーネントを使っているのと、子、孫でpropsを伝搬させていない点が異なります。

上のコードではChildとGrandsonは何もしていないので、書く必要はないのですが、「バケツリレーしなくてもいいよ感」を出すためにわざわざ書いています。

APIの簡単な解説

React.createContext

この関数を実行すると、後述するProviderとConsumerをペアで返します。
上記のコードのように2値を一気に受け取る方法もありますが、下のように名前をつける方法もあります。
複数のContextを利用する場合はこっちの方が良いかもしれません。

このAPIは引数にテスト時などに使うdefault valueを取ります。
「初期値」ではないことに注意が必要です。

Provider

Consumerにcontextを渡す側のコンポーネントです。
valueを使ってデータを渡すことができます。
上の例のような値だけでなく、関数も渡すことができます。

Reduxが用意しているProviderコンポーネントと同じような役割で、全く同じ名前なのは…🤬🤬🤬

Consumer

Providerからcontextを受け取ります。
ちょっと変わったことに中にはjsxではなく関数を書きます。

この、ちょっと特殊な使い方をするConsumerの内部については以下の記事で解説されています。

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

ちなみに

公式のDocumentの例では、「テーマカラー」や「ログイン中ユーザー」など、「状態が頻繁に変わるわけではないが、複数のコンポーネントからアクセスされるデータ」をContextとして扱っています。

ですが、これに限らず使えると思います。

少し拡張したコードを書いてみる

次の例ではまだ紹介していないAPIであるClass.contextTypeを使ってみるのと、値の他に関数も渡した例を書いてみます。

github: SlightlyExtendedContextAPI/index.jsx

よくある「ボタンを押せばインクリメントするやつ」です。
stateを定義し、関数もcontextとしてConsumerに渡しています。

動かしたイメージです。

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も使っています。

さっきの例に比べてだいぶ簡素に書けました。
親もclassを使わずに関数コンポーネントで書けます。

useStateに関しては前に書いたこの記事などを参考にしてみてください。
ここでは、useContextを見てみます。

ちょっとした解説

useContextの引数部分には、createContextで作ったContextの名前を設定します。
これだけで、contextにアクセスできるようになります。

孫コンポーネントを再掲します。

今までは孫は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と接続します。

肝は子供側です。
react-reduxからReactReduxContextをimportして使います。
本来の書き方だとconnectしたりする必要がありましたが、その辺を書かずにstateにアクセスできるようになりました。

Consumerの中のstoreStateってなんだ??って感じですが、これはProviderコンポーネントの内部コードを読めばわかります。
以下のコードはProviderコンポーネントの内部コードの一部です。

このバージョンからProviderコンポーネントの定義にContextAPIが使われるようになりました。
Providerコンポーネントの中のstateにstoreStateを定義し、render()の中でContext.Providerを返しているのがわかります。

これによってReactReduxContextコンポーネントを使えば、どこからでもstateにアクセスすることができますね。

注意なのは、actionにはアクセス出来ないということです。
actionを使うためには今まで通りconnectする必要があります。

今回の例でも、modulesの中でincrementするactionを定義していますが、これは使っていません。

ちなみに、さきほどのuseContextを使って書き直せば以下のようになります。
若干、簡潔になり普段どおりのjsxで書けますが、contextの入れ子が1段深まります。
どっちが良いかは好み分かれそうですね。

で、結局いつ使うのか?

長々書いてきましたが、結局ContextAPIはいつ使えばいいのでしょうか

ここまでいろいろ書いた上で言うのもあれですが、結局は今後もReduxを使っていくことになる気がします。
ですが、Reduxを使いたくないくらいの小規模のプロジェクトを作るならContextAPIが重宝するかもしれません。

また、Reduxを使う場合でも上の例のようにReactReduxContextを使い、部分的にContextAPIを使うことで便利に書けたりできそうです。

ここに関してはReduxがどうのこうのというよりreact-reduxが今後どう対応していくかにかかっている気がします。
が、Reactさん的にはReduxを使わずとも便利に扱える方向にしていきたいのでしょうか。
まだ、いずれ大きな変更があるかもしれません。