というライブラリを作りました。
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 になりました。
なので、内部的に提供される 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 お願いします。