ライブラリのコードを読み解くNo.1 <Redux createStore編>

## はじめに

ライブラリのコードを読み解いて、より深くライブラリについて理解するという記事です。

今回は状態管理のためのライブラリであるReduxのcreateStoreについて記載します。



**この記事で説明すること**

- ReduxのcreateStoreのソースコードの説明



**この記事で説明しないこと**

- Reduxのコンセプトや概要の説明
- createStoreの使用方法やサンプルコードの解説

Reduxの概要を説明した記事はいろいろあるかと思いますので、[公式サイト](https://redux.js.org/)やいろいろな解説記事を見ていただくのが良いと思います。

個人的には、Reduxのco-maintainerであるMark Erikson氏によって書かれた[こちらのスライド](https://blog.isquaredsoftware.com/presentations/2018-03-redux-fundamentals/#/19)がわかりやすかったです。


**対象読者**

- Reduxのコンセプト説明などのページを見て概要はなんとなくはわかっている人
- ReduxのAPIを使用したことがある人

**ライブラリバージョン**

- Redux 4.0.5 (2020年1月25日時点での最新バージョン)



## APIの説明

#### APIの定義

Reduxの公式サイトからの引用となりますが、APIの定義は以下のようになります。

> createStore(reducer, [preloadedState], [enhancer])
>
> Creates a Redux [store](https://redux.js.org/api/store) that holds the complete state tree of your app. There should only be a single store in your app.

[createStoreのAPIリファレンス](https://redux.js.org/api/createstore)



#### APIの使用方法

APIの使用方法としては以下のようになります。

(ReduxはReactと組み合わせて使われることも多いですが、公式サイトにも書かれている通りReactにしか使えないというものではありません。そのため、下記のコードにはReactのコードは入っていません)

```javascript:index.js
import { createStore } from 'redux'
import todoApp from './reducers'
import {
  addTodo,
  toggleTodo,
} from './actions'

const store = createStore(todoApp)

console.log(store.getState())

const unsubscribe = store.subscribe(() => console.log(store.getState()))

store.dispatch(addTodo('Learn about actions'))
store.dispatch(toggleTodo(0))

unsubscribe()
```



```javascript:actions.js
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'

export function addTodo(text) {
  return { type: ADD_TODO, text }
}

export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}
```



```javascript:reducers.js
import {
  ADD_TODO,
  TOGGLE_TODO,
} from './actions'

function reducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          text: action.text,
          completed: false
        }
      ]
    case TOGGLE_TODO:
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completed: !todo.completed
          })
        }
        return todo
      })
    default:
      return state
  }
}

export default reducer
```



#### createReducerのソースコードと説明

[createReducerのコード](https://github.com/reduxjs/redux/blob/master/src/createStore.ts)を順番に見ていきます。

コード内に注目する点をコメントとして記述しましたので、読み解く際の参考にしていただければと思います。

##### 1 createStore

```javascript:createStore.ts
export default function createStore {
  ...
  let currentReducer = reducer
  // (1)createStoreの引数で受け取ったpreloadedStateをStateの初期値にセットする
  let currentState = preloadedState as S
  let currentListeners: (() => void)[] | null = []
  let nextListeners = currentListeners
  let isDispatching = false
  ...
  // (2)storeが保持する関数を定義する
  function getState(): S {
    ...
  }

  ...
  function subscribe(listener: () => void) {
    ...
  }

  ...
  function dispatch(action: A) {
    ...
  }

  // (3)reducerを実行してreducer内で初期値をセットしてreturnする
  // When a store is created, an "INIT" action is dispatched so that every
  // reducer returns their initial state. This effectively populates
  // the initial state tree.
  dispatch({ type: ActionTypes.INIT } as A)

  // (4)dispatch, subscribe, getState等の関数をreturnする
  const store = ({
    dispatch: dispatch as Dispatch<A>,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  } as unknown) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
  return store
}
```

(1)と(3)でStateの初期化が2度行われています。これは以下のような動きになります。

- createStoreの引数にpreloadedStateが与えられたとき

  - Stateの初期値はpreloadedStateになる

- createStoreの引数にpreloadedStateが与えられなかったとき

  - Stateの初期値はreducer内で初期化した値になる

  

reducerではES6の構文を使用してよく`function myReducer(state = someDefaultValue, action)`という形でstateに初期値が与えられます。createStoreにpreloadedStateが与えられなかったときには、myReducerの引数に渡されるstateがundefinedの状態になりますので、reducerはstateの初期値にsomeDefaultValueを入れて、この値をreturnすることになります。

より詳しい解説はReduxの公式サイトの[ Initializing State](https://redux.js.org/recipes/structuring-reducers/initializing-state)をご覧ください。


シーケンスは以下のようになります。
![createStoreシーケンス図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/7f8c4a84-607d-fce5-79ef-18641cd98c6b.png)




##### 2 store.subscribe

```javascript:createStore.ts
function subscribe(listener: () => void) {
  ...
  let isSubscribed = true

  ensureCanMutateNextListeners()
  // (1)storeオブジェクト内のlistenrを保持している配列にlistenrを追加する
  nextListeners.push(listener)

  // (2)unsubscribeをするための関数をreturnする
  return function unsubscribe() {
    ...
    isSubscribed = false

    ensureCanMutateNextListeners()
    const index = nextListeners.indexOf(listener)
    // (3)storeオブジェクト内のlistenrを保持している配列からlistenrを削除する
    nextListeners.splice(index, 1)
    currentListeners = null
  }
}
```
storeオブジェクト内に保持している配列にlistenerを追加したり削除したりしているだけですね。


##### 3 store.dispatch

```javascript:createStore.ts
function dispatch(action: A) {
  ...
  try {
    isDispatching = true
    // (1)reducerを実行して、返り値として新しいstateを受け取る
    currentState = currentReducer(currentState, action)
  } finally {
    isDispatching = false
  }

  const listeners = (currentListeners = nextListeners)
  // (2) subscribeによってよって登録されたlistenerを呼び出し、stateが更新されたことを通知する
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i]
    listener()
  }

  return action
}
```



シーケンスは以下のようになります。
![dispatchシーケンス図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513800/92a9d457-7a58-e948-e060-dcab8e1968db.png)

Storeからreducerを実行して更新されたStateを受け取ったら、listenerに登録されている関数を順番に呼び出すだけです。このとき、更新されたstateはlistenerには渡されません。listener内でstateを取得したい場合はgetState()を呼び出す必要があります。

※　ReduxをReactと組み合わせて使用する場合には、Listenerの部分は基本的にはUIコンポーネントになります。


##### 4 store.getState

```javascript:createStore.ts
function getState(): S {
  if (isDispatching) {
    throw new Error(
      'You may not call store.getState() while the reducer is executing. ' +
        'The reducer has already received the state as an argument. ' +
        'Pass it down from the top reducer instead of reading it from the store.'
    )
  }

  // (1)storeオブジェクト内に保持しているcurrentStateをそのまま返す
  return currentState as S
}
```
currentStateを返しているだけですね。



## 参考情報

- [Reduxの公式サイト](https://redux.js.org/)
- [createStoreのAPIリファレンス](https://redux.js.org/api/store)