React
reactnative
redux

React v16で実装された new Context APIを使って、Reduxへ別れを告げる

alt
React v16でContext APIが新しくなりました。
このContextを使うことで、これまでサードパーティーのライブラリに頼っていたstate管理をreact本来の機能で実装できるようになりました。
しかしfacebookの例により、シンプルで柔軟性に富む代わりに実際の運用が難しいです。

それではreactのContext APIをstate管理に使う際のメリットデメリットについて書きます。
メリット
・記述量が圧倒的に少ない。reduxの1/3。
・stateだけでなく、変数を渡せる。
・可読性が高くシンプルなコードが書ける。
・学習コストが低い。
デメリット
・ContainerコンポーネントでJSXが純粋なViewでなくなり、ネストが2段深くなる。
・あるstateを変更すると、Provider以下全てのコンポーネントを更新する為アプリが遅くなる。observedBitsで解決できるが、現在unstableな上に記述がやや複雑になる(bitcode, 0b01bなどを使う)。
・storeが一つでないのでスパゲッティになりやすい。

Context APIの要素

Context APIは三つの要素のみで成り立ちます。

1. React.createContext

後述するProvider, Consumerをペアで作ります。
例 const {Provider, Consumer} = React.createContext;
// 以下のように名前をつけることもできます。
const RootCotext = React.createContext;

2. Provider

Consumerに、contextを渡す為のコンポーネントです。
reduxのprovider, createStoreの役割です。

3. Consumer

Providerからcontextを受け取ります。
reduxのconnect,mapStateToProps,mapDispatchToPropsの役割です。

それでは実例を見てみましょう。

Text はReactNativeの要素で文字列を表示します。

// App.js
const Son = (props) => (
  <Text>{props.name}</Text>
);

const Father = (props) => (
  <Son name={props.name}/>
);

export default class GrandFather extends Component {
  render() {
    return (
      <Father name='カバ' />
  );
}

従来はこのようにpropsをバケツリレーして渡していました。
次にContextを使って擬似global stateを実現してみます。

// App.js
const Son = () => (
  <RootContext.Consumer>
    {context => (
      <Text>{context}</Text>
    )}
  </RootContext.Consumer>
);

const Father = () => (
  <Son />
);

export default class GrandFather extends Component {
  render() {
    return (
      <RootContext.Provider name='カバ'>
        <Father />
      <RootContext.Provider>
  );
}

これで祖父コンポーネントから孫コンポーネントへstate(今回は変数)を渡すことができるようになりました。
このように擬似global変数を作れました。
ちなみにコード内で使われているname, contextは好きな文字列を使ってokです。
reduxにおけるdispatchを実現するには、() => this.setState()を子コンポーネントに渡します。

それではより実践的な例を見てみましょう。
updateはスプレッド演算子を使った記述で、contextのメソッドではありません。

reduxのようにstoreという名前のオブジェクトを作り切り出します。contextにオブジェクトと関数を渡せることを利用して擬似storeを作っています。ただreduxのcombineReducerがない分規模が大きくなるほど辛くなる。

const store = {
  state = {
    foo: 1,
  },
  // スプレッド演算子と組み合わせ、stateを変更します。
  update(cb) {
    this.state = cb(this.state);
  }
};
const {Provider, Consumer} = React.createContext(store.state);
const Foo = () => (
  <Consumer>
    {({foo, update}) => (
      <div>
        <button
          onClick={() =>
            update((state) => ({...state, foo: state.foo + 1}))
          }
        >
          click
        </button>
        <p>Foo is {foo}</p>
      </div>
    )}
  </Consumer>
);

const App = () => (
  <Provider value={store.state}>
    <Foo />
  </Provider>
);

だいぶ実践的になりましたね。

結論

小規模アプリの開発には向いている。
非同期もasync, awaitで解決できそうだ。
combineReducerもどきも独自ルールで作れる。
しかし、それ以外の用途ではredux, mobxを使用した方が使い勝手がいい。
結局シンプルすぎて、独自ルールを追加しないとやっていけないのが辛い。
チーム開発では推奨できない。
だだし、疎結合なので、reduxへの移行も簡単だ。
十分に実用的なので、個人では使っていきたい。