38
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

use-react-redux-context で redux 環境下で React Hooks を使う

Last updated at Posted at 2019-03-01

というライブラリを作りました。

yarn add use-react-redux-context

で使えます。が、 peer deps として react react-dom react-redux redux がいるので、今までの redux スタック全部盛りでもあります。

これは何

ReactRedux connect を、Context API と useContext で置き換えようとしたものです。

その過程でパフォーマンスチューニングする余地を大量に入れ込んでいます。

hooks で redux を置き換えるものではなく、react-redux と協調するものです。

簡単な使い方

ReactRedux の Provider の下で、mapState される props を持つ Context を生成し、それを useContext できる、というもの。

import { Provider as ContextProvider, Scope } from "use-react-redux-context";
import { Provider as ReactReduxProvider } from "react-redux";

const scope = new Scope();
const RootStateContext = scope.createContext(s => s);
const All = () => {
  const all = useContext(RootStateContext);
  return <pre>{JSON.stringify(all)}</pre>;
};

const store = createStore(reducer); // create your reducer
const el = (
  <ReactReduxProvider store={store}>
    <ContextProvider scope={scope}>
      <All />
    </ContextProvider>
  </ReactReduxProvider>
);

直接 React.Context を生成するので、そのまま useContext できます。

仕組み

(ここから先は雑に使うだけなら読まなくても大丈夫です。)

react-redux@6 以降は、親子関係に基づくデータの受け渡し方法が、実装が古い prop-types ベースの  Context API から New Context API になりました。

Context – React

なので、内部的に提供される ReactReduxContext と hooks の useContext を使うと、次のように使うことができます。

import React, {useContext} from "react"
import { ReactReduxContext, Provider } from "react-redux";
functin App(){
  const {store, storeState} = useContext();
  return <pre>{JSON.stringify(storeState)}</pre>
}

ReactDOM.render(<Provider store={...}><App /></Provider>, el);

ただ、 ReactReduxContext をこのまま使うと、 useContext したすべてのコンポーネントは、redux のすべての変更に対して render が走ってしまうので、すぐにパフォーマンス問題にぶち当たってしまいます。

既存の connect の最適化手法

react-redux の connect は、mapState された値の shallow equal 比較によって再更新を抑制していました。

const MyConnectedComponent = connect(s => ({value: s.counter.value}))(MyComponent);

このとき MyComponent が更新されるのは prevState.value !== nextState.value のときだけです。

最適化: 事前に mapState される Context を、その数だけ作ればいいのでは

use-react-redux-context の思想は、 Context が事前に宣言されたスコープになるなら、それを全部事前に宣言してしまえばいいのでは、というものです。

provider の scope という概念を導入しました。

const scope = new Scope();
const RootStateContext = scope.createContext(s => s);

const All = () => {
  const all = useContext(RootStateContext);
  return <pre>{JSON.stringify(all)}</pre>;
};

この scope に紐づく Context は、redux store が更新されるたびに、一度だけ実行されて状態が更新されます。

これによって、connect されるだけ計算される mapState の実行回数が、宣言される Context の種類の数になりました。

この制約によって、 Component 側の要求によって、 connect を動的に宣言する、という感じの世界観はなくなります。できるにはできますが、シングルトンに依存する次のようなコードになると思います。

import {myScope} from './contexts/my-scope'
const AContext = myScope.createContext(s => s.a);
export function A() {
  const a = useContext(AContext);
  return ...
}

要は connect のコールバックの第二引数、 (state, ownProps) => ... の ownProps が受け取れません。ただ、ownProps を使ってる人をあまり見たことがないので、切り捨てることにしました。

そもそも scope 概念を導入したのは、異なるデータ配分の単位を分割して作りたい場合、その mapState の計算回数の N を分割する余地を残したかったからです。宣言しただけで、使わなくても必ず更新されます。

const a = new Scope();
const X = a.createContext(fn_x);
const Y = a.createContext(fn_y);

const b = new Scope();
const Z = b.createContext(fn_z);

const el = <>
  <Provider scope={a}><WorldA /></Provider>
  <Provider scope={b}><WorldB /></Provider>
</>

useCallback の mapState への応用

hooks の mapState 関数の実行は、 React Context の hooks スコープにいるということで、次のような最適化が可能になりました。

const FooContext = scope.createContext((state, dispatch) => {
  const inc = useCallback(() => dispatch(increment()), []);
  return { ...state.foo, inc };
});

これは hooks の機能を使って、memoizedKeys が同じ時は同じ関数参照を返します。

複雑さで評判の connect の第二引数 bindActionCreator を切り捨てて、(state, dispatch) => mappedState(WithAction) ということにしてしまったことで、TypeScript の推論にも優しく、使うのも簡単になりました。

で、 useCallback とこの dispatch を何度も書くのが大変なので、useBindAction, useBindActions というヘルパを作りました。

const A = scope.createContext(state => {
  const inc = useBindAction(increment);
  // with memoized keys
  // const inc = useBindAction(() => ({type: 'increment', payload: state.value}), [state.value]);
  return { ...state.a, inc };
});

const B = scope.createContext(_state => {
  const actions = useBindActions({ increment, decrement });
  // alternative: with memoized keys map
  // const actions = useBindActions({ increment, decrement }, { increment: [state.value] });

  // recommend spreading for shallow equal
  return { ...actions };
});

これで使い心地がだいぶ良くなったと思います。

最後に

Context + useReducer でだいぶ redux がいらなくなったとはいえ、redux middleware が必要だったり、異なる state が協調しづらいといった問題で、redux の需要は発展形として残り続けると思います。SSRする際も一枚のjsonにまとめないといけない以上、reduxはどうしても必要になります。

その解決策として use-react-redux-context を作ってみました。気に入ったら star お願いします。

38
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?