redux と react-redux の内部動作を解説 ～小さな “Reduxもどき” を自作してみる～

# はじめに

react-redux と redux と @reduxjs/toolkit のソースコードの主要部分を読んだので、redux と react-redux の内部動作について自分なりに解説してみようと思います。
また、redux を使わなくても少しコードを追加するだけで react-redux を動かせると思ったので試してみました。

## 注意・前提
本稿は hooks API を前提にしており、**connect API のことは考慮しておりません**。[^connect]

前提とするバージョンは以下の通りです。
- react-redux: 7.2.3
- redux: 4.0.4

[^connect]: [公式サイト(💡TIP)](https://react-redux.js.org/tutorials/connect#tutorial-using-the-connect-api)では「React-Redux hooks APIをデフォルトとして使用すること」が勧められているので hooks API の理解を優先しました。

## 対象読者
###「Redux の基本的な動作（react-redux と関係する部分のみ解説）」まで
- redux と react-redux（hooks API） は使ったことがあるけど、redux と react-redux の内部的な動作は分からないという方

###「“Reduxもどき” を作成して動かしてみる」以降
- [Redux Toolkit Quick Start](https://redux-toolkit.js.org/tutorials/quick-start) のソースコードを読んだことのある方

[^linked list]: react-redux では Array ではなく、[linked list](https://github.com/reduxjs/react-redux/pull/1523) が使われています
[^reduxの場合]: 少なくとも redux 及び react-redux の場合「全て」実行されます


# react-redux の Provier が store（createStore の戻り値）に求めているプロパティ【必須３メソッド】

Provider.js の PropTypes を見てみると、`store` インスタンスのプロパティとして **`subscribe`、`dispatch`、`getState` （以下、「必須３メソッド」と呼称） が必要である**ことが分かります。「必須３メソッド」についての概要は後述します。

```js:Provider.js（一部抜粋）
if (process.env.NODE_ENV !== 'production') {
  Provider.propTypes = {
    store: PropTypes.shape({
      subscribe: PropTypes.func.isRequired,
      dispatch: PropTypes.func.isRequired,
      getState: PropTypes.func.isRequired,
    }),
    context: PropTypes.object,
    children: PropTypes.any,
  }
}
```

# Redux の基本的な動作（react-redux と関係する部分のみ解説）

## State (Store内部) の変化の様子

redux では reducer（(B, A) => B） を使って store 内で保持している state （以下 `currentState` と記載）を更新します。
単に reducer と言った時には、`A`、`B` は任意の型ですが、redux では `B` は `currentState`、`A` は Action です。
`currentState` が reducer と Action によって initialState -> State1 -> State2 と変遷する様子を図示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1425145/c4e2fa73-dae9-147c-41a1-4c0f9c6029d0.png)
[^mermaid 1]
[^mermaid 1]: mermaidを利用 graph TD
    S0[B: initialState]-->R1((reducer: B, A => B))
    A1[A: Action1] -- type:x1 payload: y1 -->R1
    R1 --> S1[B: State1]
    S1-->R2((reducer: B, A => B))
    A2[A: Action2] -- type:x2 payload: y2 -->R2
    R2 --> S2[B: State2]

## 必須３メソッド（getState、subscribe、dispatch）の概要

※ 分かりやすさ優先のため、説明で用いる変数名は実際の変数名とは異なる場合があります。すべてのパターンを網羅した完全な説明ではなく、あくまでも概要です。

### `getState`
store の内部変数 `currentState` を取得します。

[Redux Toolkit Quick Start](https://redux-toolkit.js.org/tutorials/quick-start) においては、`currentState` は `{ counter: { value: number } }` です。

### `subscribe`
`store` の 内部変数[^内部変数]`listeners`[^実際] に `notify`関数（後述）を“登録”（※）します。
最初に実行される `useSelector` hook にて実行されます。[^connectお断り]

※ redux の `listeners` は Array なので、“登録”というのは、`listeners.push(notify関数)` を意味します。

なお、戻り値は `subscribe` で登録した `notify`関数 を `listeners`から 除去する `unsubscribe`関数 です。


#### `notify`関数 について
redux には Observer パターン が用いられていますが、**redux-react にも Observer パターン が用いられていて[^Observer パターン]、redux-react は内部に `lisners` をもっています。** redux-react 内の `lisners` には `useSelector` hook にて `checkForUpdates`関数が登録されます[^connectお断り] （`checkForUpdates`関数については後述）。react-redux 内の `lisners`（`checkForUpdates`関数の配列[^正確には linked list]） を全て実行する関数が `notify`関数 です。

[^正確には linked list]: 正確には [linked list](https://github.com/reduxjs/react-redux/pull/1523)。
[^実際]: 実際の変数名とは異なります。ここでは理解しやすさを優先しています。
[^connectお断り]: 「はじめに > 注意」でお断りを入れた通り、connect API は説明対象外にしています。

### `dispatch`
`useDispatch` hook で取得する、`dispatch` です。
大きく分けて以下の２つのことを行います。
1. `currentState = reducer(currentState, action)` [^実際]
2. store 内部変数 `lisners`[^実際] に登録された全ての関数を実行（※）
(3. 引数の action をそのまま return)

※ redux(store)内の`listeners` には、基本的には `notify`関数 の１つしか登録されないため、実際に実行される関数は `notify`関数 だけです。（ただし、react-redux 以外のコードが `subscribe` を使用している場合はこの限りではありません。）

### （useSelector 内の）checkForUpdates 関数 の概要

1. `store.getState()` で `currentState` を取得 （`store` はレンダー時に Provider（Contex）から取得）
2. `useSelector` 第一引数の `selector` を用いて `currentState` から 対象データを取得し、`latestSelectedState.current` に保存
3. 前回保存対象データ（※１）と 最新対象データ(2.) を比較 （デフォルト：Strict equality (===) で比較 ／ 第二引数 equalityFn が渡されていれば、それを利用して比較）
4. (3.) の比較結果が false なら（対象データが前回と異なるデータなら） `forceRender`（※２）で強制的にコンポーネントを再レンダーする

※１ 前回実行時、(2.)にて `latestSelectedState.current` に保存したデータ
※２ forceRender: `const [, forceRender] = useReducer((s) => s + 1, 0)`

### 必須３メソッドまとめ

||redux (store) | react-redux
|---|---|---|
|lisners に登録される関数 |notify（react-redux 内の listeners（checkForUpdatesたち）を全て実行する関数）| checkForUpdates |
|lisners に登録される関数の数 |１つ（Provider につき１つ）| useSelector 等の個数 |
|lisners に登録された関数が実行されるタイミング |dispatch 実行時| dispatch 実行時|
dispatchの実行 -> listenrs(redux)の実行 -> `notify`関数の実行 -> lisners(react-redux)の実行 -> 全てのcheckForUpdatesの実行 という流れになります。
つまり、**`useSelector` 等で登録された `checkForUpdates` 関数たちが `dispatch` で全部実行されるということになります。**
`dispatch` を実行すると、useSelector の数だけ[^connectお断り]、「selectorによるデータ取得及びデータの比較」 が実行されます。

# “Reduxもどき” を作成して動かしてみる

「Redux の基本的な動作（react-redux と関係する部分のみ解説）」を踏まえて Redux を模倣した 簡単なソースコードを書いてみます。

## State (Store内部) の更新方針

“Reduxもどき” では、簡略化のため、`(prevState: S) => S` という形の関数（以下、modifier と呼称）で store の内部変数 `currentState` を更新します。
`currentState` を変更させる関数として、
`(B, A) => B`[^ts1] (reducer)
ではなく、
[^ts1]: TypeScript表記: `type Reducer<A, B> = (b: B, a: A) => B;`
`(A) => (B) => B` [^ts2] (modifierCreator)
[^ts2]: TypeScript表記: `type CreateEndomorphism<A, B> = (a: A) => (b: B) => B;`
という形の関数を使います。
`A` は任意の型であり、Action（`{type: "name", payload: something}`のようなオブジェクト）である必要はありません。
`B` は `currentState` です。
以下は modifier の例です。

```javascript
const [count, setCount] = React.useState(0);
const add = (a) => (b) => a + b // modifierCreator: (A) => (B) => B
const inclement = add(1) // 👈 modifier: (B) => B （ここでは、(number) => number）
// modifier の使い方： setCount(inclement)
```

`currentState` が modifier によって initialState -> State1 -> State2 と変遷する様子を図示します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1425145/cd2de3d3-74af-a2c3-ebcf-6867aaea4f5e.png)
[^mermaid 2]
[^mermaid 2]: mermaidを利用 graph TD
    S0[B: initialState]-->R1((modifier : B => B))
    R1 --> S1[B: State1]
    S1-->R2((modifier : B => B))
    R2 --> S2[B: State2]


## 必須３メソッド（getState、subscribe、dispatch）の動作方針
### `getState`
redux と同じです。（store の内部変数[^内部変数] `currentState` を取得します。）

### `subscribe`
`notify`関数 を`listeners`配列 に push するのではなく、`listener`変数 に代入します。
`listeners` に登録されるものが基本的には `notify`関数 だけであることを示すためです。

戻り値は `notify`関数 を `listener`変数 から 破棄する `unsubscribe`関数 です。

### `dispatch`
引数として Action ではなく、modifier （`(B)=>B`の関数）を渡します。
大きく分けて２つのことを行います。
1. `currentState = modifier(currentState)`
2. `listener`変数 に代入された `notify`関数 を実行

### 必須３メソッドにおける主な相違点まとめ
redux との 一番の違いは `dispatch` における `currentState` の変更方法が
reducer（(B, A) => B） から modifier（(B) => B） になっている部分です。

||redux | reduxもどき
|---|---|---|
|getState |（`currentState`の取得） | （相違点なし） |
|subscribe |`listeners`配列 に `notify`関数 を push | `listener`変数 に `notify`関数 を 代入  |
|dispatch |currentState = reducer(currentState, action) | currentState = modifier(currentState)|

## ソースコードの変更

以上を踏まえて redux の `createStore` を自作してみます。
### src/app/store.js の変更（createStore の自作）

[変更前のソース（Github）](https://github.com/fizumi/redux-essentials-counter-example/blob/master/src/app/store.js)

```js:src/app/store.js
import { configureStore } from '@reduxjs/toolkit'; // redux の `createStore` を内部で利用
import counterReducer from '../features/counter/counterSlice';

export default configureStore({
  reducer: {
    counter: counterReducer,
  },
});
```

[変更後のソース（Github）](https://github.com/fizumi/redux-essentials-counter-example/blob/endomorphism/src/app/store.js)

```js:src/app/store.js
import initialState from '../features/counter/counterSlice'; // counterSlice は後述

// 前述の「必須３メソッド（getState、subscribe、dispatch）の動作方針」通りに実装
const createStore = initialValue => {
  let currentState = initialValue; // state を初期化
  let listener; //  notify関数 の代入先内部変数
  const store = {
    getState: () => currentState,
    subscribe: notify => {
      listener = notify; // notify関数 を代入
      return function unsubscribe() {
        listener = undefined; // notify関数 を破棄
      };
    },
    dispatch: modifier => {
      currentState = modifier(currentState); // state を更新
      listener && listener(); // notify関数 を実行
    },
  };
  return store;
};

export default createStore(initialState);
```


### src/features/counter/counterSlice.js の変更
[変更前のソース（Github）](https://github.com/fizumi/redux-essentials-counter-example/blob/master/src/features/counter/counterSlice.js)

```js:src/features/counter/counterSlice.js（コメントは省略）
import { createSlice } from '@reduxjs/toolkit';

export const slice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    increment: state => {
      state.value += 1;
    },
    decrement: state => {
      state.value -= 1;
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = slice.actions;

export const incrementAsync = amount => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount));
  }, 1000);
};

export const selectCount = state => state.counter.value;

export default slice.reducer;
```

[変更後のソース（Github）](https://github.com/fizumi/redux-essentials-counter-example/blob/endomorphism/src/features/counter/counterSlice.js)

```js:src/features/counter/counterSlice.js
// import { createSlice } from '@reduxjs/toolkit'; // -
import produce from 'immer';                       // + 
// immer は redux toolkit 内部で利用されているライブラリ

export const entier = {
  // initialState: B = { counter: { value: number } }  slice ではなく、state 全体
  initialState: { 
    counter: { value: 0 },
  },
  // modifierCreator: A => B => B
  modifierCreators: {
    //         A => B     => B          (immer なしの場合 / A は不要)
    increment: _ => state => ({ 
      ...state,
      counter: {
        ...state.counter,
        value: state.counter.value + 1,
      },
    }),
    //         A =>         B     => B  (immer ありの場合 / A は不要)
    decrement: _ => produce(state => {
        state.counter.value -= 1;
      }),
    //                 A      =>         B     => B     （ A は number ）
    incrementByAmount: amount => produce(state => {
        state.counter.value += amount;
      }),
  },
};

// dispatch には action creator の戻り値ではなく、modifier creator の戻り値を渡す
// export const { increment, decrement, incrementByAmount } = slice.actions;        // -
export const { increment, decrement, incrementByAmount } = entier.modifierCreators; // +  

// incrementAsync  変更なし

// selectCount  変更なし

// export default slice.reducer;    // -
export default entier.initialState; // +
```

### src/features/counter/Counter.js の変更

@reduxjs/toolkit の `configureStore` が返す `store` の `dispatch` は redux-thunk を含む middleware が合成された `dispatch` です（※）。今回実装した dispatch にはそのような合成を行っていないので、redux-thunk の合成なしで動くような書き方に変更します。

```diff:src/features/counter/Counter.js
-  onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
+  onClick={() => incrementAsync(Number(incrementAmount) || 0)(dispatch)}
```
※ この説明では知ってる人にしか分からないと思いますが、本稿では Async を扱うロジックの詳細な説明は割愛させていただきます。

### デモ および オリジナルとの差分

以上３つのファイルの修正で redux なしで react-redux が動くようになりました。
[デモはこちら（CodeSandbox）](https://codesandbox.io/s/github/fizumi/redux-essentials-counter-example/tree/endomorphism)を参照ください。
[オリジナル](https://github.com/reduxjs/redux-essentials-counter-example/tree/master/) との [差分はこちら](https://github.com/fizumi/redux-essentials-counter-example/compare/master...endomorphism) を参照ください。

### おまけ： ramda ライブラリ を利用した場合

JavaScript プログラマのための関数型ライブラリである [ramda](https://github.com/ramda/ramda) を使うと
変更後の src/features/counter/counterSlice.js をスッキリ記述することができます。

```js:src/features/counter/counterSlice.js
import * as R from 'ramda';

const lens = R.lensPath(['counter', 'value']); 

export const entier = {
  // initialState: B = { counter: { value: number } }
  initialState: {
    counter: { value: 0 },
  },
  // modifierCreator: A => B => B
  modifierCreators: {
    increment: _ => R.over(lens, R.inc),
    decrement: _ => R.over(lens, R.dec),
    incrementByAmount: amount => R.over(lens, R.add(amount)),
  },
};

export const { increment, decrement, incrementByAmount } = entier.modifierCreators;

// incrementAsync 変更なし

export const selectCount = R.view(lens);

export default entier.initialState;
```
[変更後ソースコード（immer 利用バージョン）と 変更後ソースコード（ramda 利用バージョン）との差分はこちら](https://github.com/fizumi/redux-essentials-counter-example/compare/endomorphism...endomorphism-with-ramda) を参照ください。
[デモはこちら（CodeSandbox）](https://codesandbox.io/s/github/fizumi/redux-essentials-counter-example/tree/endomorphism-with-ramda)を参照ください。

#### ramda のススメ
[ramda のドキュメント](https://ramdajs.com/docs/) は検索性に優れ、全ての関数に説明と例が記載されており、しかも「Run it here」ですぐに動作を試せるという至れり尽くせりなドキュメントです。
ramda の全ての関数は破壊的な変更を行わないので、React の state の更新にもってこいです。
例えば [evolve](https://ramdajs.com/docs/#evolve) を使えば 下記の `increment1` のような書き方をせずに react の state を更新できます。

```js
import * as R from 'ramda';

const increment1 = state => ({
  ...state,
  counter: {
    ...state.counter,
    value: state.counter.value + 1,
  },
});

const increment2 = state =>
  R.evolve({ counter: { value: value => value + 1 } }, state); // 通常利用ver

const increment3 = R.evolve({ counter: { value: value => value + 1 } }); // カリー化利用ver

const increment4 = R.evolve({ counter: { value: R.inc } }); // ramda フル活用ver

// console.log(incrementX({ counter: { value: 0 } })  ->  { counter: { value: 1 }}
```
ramda の 関数は全て[カリー化](https://ja.javascript.info/currying-partials#ref-279)されていますが、通常の関数と同様に（カリー化された関数であることを意識せずに）使えるので、カリー化を知らない人も利用できます。

[^Observer パターン]: 参考: [The Observer Pattern In JavaScript as Implemented By Redux](https://www.thedevnotebook.com/2017/08/the-observer-pattern-in-javascript.html) 及び [An Obsession with Design Patterns: Redux](https://engineering.zalando.com/posts/2016/08/design-patterns-redux.html#:~:text=The%20Connect%20Method%20in%20Redux,automatically%20of%20any%20state%20changes.%E2%80%9D)。react-redux が Observer パターン を使っていると明言している記事は見つかりませんでしたが、Observer パターンに近い実装であることは間違いないと思います。
[^内部変数]: 内部変数は「クロージャ内に存在するアクセスを制限された変数」という意味合いで用いています。