はじめに
ライブラリのコードを読み解いて、より深くライブラリについて理解するという記事です。
今回は状態管理のためのライブラリである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.
APIの使用方法
APIの使用方法としては以下のようになります。
(ReduxはReactと組み合わせて使われることも多いですが、公式サイトにも書かれている通りReactにしか使えないというものではありません。そのため、下記のコードにはReactのコードは入っていません)
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()
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 }
}
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
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をご覧ください。
2 store.subscribe
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
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
}
Storeからreducerを実行して更新されたStateを受け取ったら、listenerに登録されている関数を順番に呼び出すだけです。このとき、更新されたstateはlistenerには渡されません。listener内でstateを取得したい場合はgetState()を呼び出す必要があります。
※ ReduxをReactと組み合わせて使用する場合には、Listenerの部分は基本的にはUIコンポーネントになります。
4 store.getState
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を返しているだけですね。