1
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-25

はじめに

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

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

この記事で説明すること

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

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

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

Reduxの概要を説明した記事はいろいろあるかと思いますので、公式サイトやいろいろな解説記事を見ていただくのが良いと思います。

個人的には、Reduxのco-maintainerであるMark Erikson氏によって書かれたこちらのスライドがわかりやすかったです。

対象読者

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

ライブラリバージョン

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

APIの説明

APIの定義

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

createStore(reducer, [preloadedState], [enhancer])

Creates a Redux store that holds the complete state tree of your app. There should only be a single store in your app.

createStoreのAPIリファレンス

APIの使用方法

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

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

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()
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 }
}
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のコードを順番に見ていきます。

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

1 createStore
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をご覧ください。

シーケンスは以下のようになります。
createStoreシーケンス図.png

2 store.subscribe
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
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

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

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

4 store.getState
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を返しているだけですね。

参考情報

1
1
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
1
1