Reactの新しいContext APIを学ぶ

React v16.3ではContext APIが刷新され、コンポーネント間のデータの受け渡しが
バケツリレー的な手法から直接データストアから状態を取得できるようになっている。
ということで、まず試してみる。

まずおさらいをする。

従来のデータの受け渡し

Reactでは親コンポーネントから子コンポーネント、孫コンポーネントにデータを渡す際には、
バケツリレー(Reactドリル)的な方法で参照を渡していく必要があった。

簡単なネストコンポーネントを使っておさらいしてみる。

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
import React, { Component } from 'react';

// Data Flow
// Animal Component --> Homo Component --> Sapiens Component

const Sapiens = props => (
<div className="sapiens">{props.name}</div>
)

const Homo = props => (
<div className="homo">
<Sapiens name={props.values.name} />
</div>
)

class Animal extends Component {
state = {
values: {
name: 'Lucy'
}
}

render() {
return (
<div className="animal">
state: {this.state.values.name}
<Homo values={this.state.values} />
</div>
);
}
}

export default Animal;

上記の例のようにコンポーネント間でprops経由でデータを渡す必要がある。
これらを解決する方法として、これまでReduxなどのライブラリを使う必要があった。

新しいContext APIを使う

React New Contetx APIで状態を管理、参照するには、まずデータストアを定義する必要があります。
このデータはあたかもグローバルな変数のように扱えるため、子コンポーネント、孫コンポーネントと参照を渡す必要はない。

createContext

まずcontextを作成する。これによりProviderとConsumerのペアを生成します。

1
const {Provider, Consumer} = React.createContext(defaultValue)

今回はカスタムオブジェクトからProviderとConsumerを使います。

1
const HomoSapinsesContext = React.createContext()

Providerの設定

ProviderをラップするカスタムProviderを作成します。
ProviderはConsumerにデータを提供する機能。
valueプロパティとしてstateを渡している点がそれにあたる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HomoSapinsesProvider extends Component {
state = {
values: {
name: 'Lucy'
}
}

render() {
return (
<HomoSapinsesContext.Provider
value={this.state.values}
>
{this.props.children}
</HomoSapinsesContext.Provider>
)
}
}

Consumerでコンポーネントからデータを取得する

Providerの配下に子コンポーネントしてConsumerを配置することで、
グローバルな感じでデータを受け取ることができる。
このデータが変更されるたびに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
// Consumerを経由して直接データを参照できている
const Sapiens = () => (
<div className="sapiens">
<HomoSapinsesContext.Consumer>
{(context) => context.name}
</HomoSapinsesContext.Consumer>
</div>
)

// 下位のコンポーネントにデータを渡してない
const Homo = () => (
<div className="homo">
<Sapiens />
</div>
)

class ContextAnimal extends Component {
render() {
return (
<HomoSapinsesProvider>
<div>
<span>state:
<HomoSapinsesContext.Consumer>
{(context) => context.name}
</HomoSapinsesContext.Consumer>
</span>
<Homo />
</div>
</HomoSapinsesProvider>
)
}
}

このように上位のコンポーネントからデータを受け取らないで直接データにアクセスできる。

ここまでのコードの全体は以下のとおり。

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
48
49
50
51
52
import React, { Component } from 'react';

const HomoSapinsesContext = React.createContext()

class HomoSapinsesProvider extends Component {
state = {
values: {
name: 'Lucy'
}
}

render() {
return (
<HomoSapinsesContext.Provider value={this.state.values}>
{this.props.children}
</HomoSapinsesContext.Provider>
)
}
}

const Sapiens = () => (
<div className="sapiens">
<HomoSapinsesContext.Consumer>
{(context) => context.name}
</HomoSapinsesContext.Consumer>
</div>
)

const Homo = () => (
<div className="homo">
<Sapiens />
</div>
)

class ContextAnimal extends Component {
render() {
return (
<HomoSapinsesProvider>
<div>
<span>state:
<HomoSapinsesContext.Consumer>
{(context) => context.name}
</HomoSapinsesContext.Consumer>
</span>
<Homo />
</div>
</HomoSapinsesProvider>
)
}
}

export default ContextAnimal;

状態を変更する

状態の変更にはsetStateで直接状態を変更する。
この辺りのやり方は定まってないため、開発者に依存しそう。

Reduxのように状態変更のためのReducerを作成し、それを利用してみることにします。

1
2
3
4
5
6
7
const reducer = (prevState, action) => {
if (action.type === 'UPDATE') {
const values = prevState.values
values.name = action.name
return { ...prevState, values };
}
}

コンポーネントにはdispatchメソッドをもたせて、
Reducerを経由して状態を変更するようにします。

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
class HomoSapinsesProvider extends Component {
state = {
values: {
name: 'Lucy'
}
}

dispatch(action)
{
this.setState(state => reducer(state, action))
}

render()
{
return (
<HomoSapinsesContext.Provider
value={
{
name: this.state.values.name,
dispatch: this.dispatch.bind(this)
}
}>
{this.props.children}
</HomoSapinsesContext.Provider>
)
}
}

Provider経由でメソッドを渡すことでConsumerから利用できる。

まとめ

新しいContext APIは複雑性がなく学習コストが低いため利用しやすいと感じ。
単純なコンポーネントやアプリケーションと比較的結合性の低いコンポーネントなどでは
そのままつかうことでデータの透明性を確保できるtのでいいかもしれない。
公式のドキュメントにもあるとおり、新しいContext APIを使うことでより容易になるパターンもある。
Redux的なアーキテクチャを利用しないと状態管理が複雑になりそうで、実施あの運用はハードルが高そう。
なので、ある程度の規模のアプリケーションや複数人での開発においてはReduxなどに乗っておくほうがいいきがする。
とはいえ、この変更は革新的なので今後は使い分けが出てくるのかなという気がした。

今回使ったサンプル

参考ページ

Comments