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

https://github.com/mizchi/use-react-redux-context

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

```
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 できる、というもの。

```jsx
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](https://reactjs.org/docs/context.html)

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

```js
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 比較によって再更新を抑制していました。

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

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

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

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

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

```typescript
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 を動的に宣言する、という感じの世界観はなくなります。できるにはできますが、シングルトンに依存する次のようなコードになると思います。

```js
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 を分割する余地を残したかったからです。宣言しただけで、使わなくても必ず更新されます。

```jsx
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 スコープにいるということで、次のような最適化が可能になりました。

```jsx
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 というヘルパを作りました。

```jsx
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 お願いします。

https://github.com/mizchi/use-react-redux-context