はじめに
react16.3から正式に提供されたcontext機能ですが、一見すると reduxの機能を置き換えるものであるようにみえます。しかし、contextはむしろ従来 render propsを使っていたケースを拡張するものであり、reduxと共存させて使うものだと思います。ここでは redux を既に利用していることを前提にどこにcontextを使うべきかということを紹介します。
redux で不便なこと
reduxはstoreという空間にアプリケーションのグローバルな状態を保持・管理します。各コンポーネントはこの状態を変更し、読みだすことで、協調した動作を実現しているわけです。しかし、あるコンポーネントが提供するAPIや機能(ex.モーダルダイアログを表示してユーザの選択を受け取るなど)を単純に呼び出したい場合、それをわざわざ状態という形にするのは労多くして益少なしです。たとえば各APIごとにAPI要呼び出しフラグみたいなものをもつだけだと、複数回同じAPI呼び出しが必要な場合に対応できません。一応 こちら でアクションをキューに積むという方法を紹介していますが、アプリケーションのグローバルな状態の保持・管理という reduxのstoreの本来の使い方に100%合致しているかといわれると、われながら自信をもって頷くことはできません。
また、reduxでは store にシリアライズ不可能なデータ、たとえば関数やオブジェクトを積むことも推奨されてません(ほとんどの場合問題なく可能ではありますが)。
render props
こういう場合ダイレクトな親子関係のあるコンポーネント間であればrender propsを使ってやりとりするのが簡単です。親から子に共有する場合は単純にpropsに加えればいいですし、逆に子から親に共有する場合は、共有対象を設定するための関数を渡します。
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
childAPI: null,
setChildAPI: api => { this.setState({'childAPI', api}) },
};
}
parentAPI(args) {
...
}
render() {
const childProps = this.state;
return (<div>
<Child {...childProps} />
</div>);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
const {parentAPI, setChildAPI} = props;
setChildAPI(this.childAPI);
...
}
childAPI(args) {
...
}
...
}
とすれば parentAPI, childAPI を相互に呼べるようになります。
同じ親をもつ子同士の間でも同様の方法で共有を行うことができます。ただしその範囲を超えると、バケツリレーが必要になり、かなり煩雑になります。
context
contextはこのpropsによるAPIの共有を直接の親子関係がなくても簡単にできるようにしたものです。親の役割を contextのProviderが果たし、子の役割をConsumerが果たします。直接の親子関係は必要ありませんがProviderとConsumerは祖先と子孫の関係である必要があります。つまり、一般的にはAPIを共有したいコンポーネントがそれぞれConsumerになり、それらの共通の祖先がProviderの役割を務めることになります。
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
childAPI1: null,
childAPI2: null,
setChildAPI: api => { this.setState({'childAPI1', api}) },
setChildAP2: api => { this.setState({'childAPI2', api}) },
};
}
render() {
return (<SomeContext.Provider value={this.state}>
// この階層下のどこかにChild1とChild2が存在する
</SomeContext.Provider>);
}
}
class Child1 extends React.Component {
constructor(props) {
super(props); // propsはreduxで使う
const {setChildAPI1} = this.context;
setChildAPI1(this.childAPI1);
...
}
childAPI1(args) {
...
}
...
}
Child1.contextType = SomeContext;
class Child2 extends React.Component {
constructor(props) {
super(props); // propsはreduxで使う
const {setChildAPI2} = this.context;
setChildAPI2(this.childAPI2);
...
}
childAPI2(args) {
...
}
...
}
Child2.contextType = SomeContext;
これでSomeContextを共有するParent, Child1, Child2の各コンポーネントの間でchildAPI1, childAPI2両方とも呼べるようになります。Parentの <SomeContext.Provider ..></SomeComponet.Provider> 内であればほかのどのコンポーネントもこの共有に参加することができます。
まとめ
以上、reduxではやりにくい特定コンポーネント間のAPIや機能の共有をcontextでやる例を紹介しました。