Django+Reactで学ぶプログラミング基礎(34): Reduxチュートリアル(TodoアプリのStore)

##### [前回] [Django+Reactで学ぶプログラミング基礎(33): Reduxチュートリアル(Todoアプリの実装(続き))](https://qiita.com/mingchun_zhao/items/dbfab155c091b7c87f8b)

## はじめに

前回は、TodoアプリのReducerを実装しました。
今回は、Storeの設計と実装です。

## 今回の内容

[Todoアプリの構築](https://redux.js.org/tutorials/fundamentals/part-4-store)

- Redux Store
    - Storeとは
    - Storeを作成
    - 初期Stateをロード
    - actionをdispatch
    - Storeの内部構造

## Storeとは

- アプリを構成するstate/action/reducerをまとめたもので、以下の機能を持つ
    - 現在のアプリstateを保持
        - `store.getState()`を介して現在のstateにアクセス
        - `store.dispatch(action)`を介してstateを更新
        - `store.subscribe(listener)`を介してリスナーコールバック(`listener callback`)関数を登録
            - コールバック関数とは
                - ある関数を呼び出す際、引数として渡される別の関数のこと
                - 呼び出し側で用意した関数を、呼び出し先のコードが`呼び出し返す`(callback)
        - `store.subscribe(listener)`によって返される`unsubscribe`関数を介してリスナーの登録を解除
    - Reduxアプリには1つのstoreしかない
        - データ処理ロジックを分割する場合は、reducerを複数に分割することで実現

## Storeを作成

- storeには単一のrootReducerが存在
    - Reduxコアライブラリの`createStore`APIで、storeを作成
        - まず、`store.js`ファイルを追加し、`createStore`と`rootReducer`をインポート
        - 次に、`createStore`を呼び出して、rootReducerを渡す
        - ※ `createStore`が`deprecated`と警告されましたが、一旦そのまま使います
```src/store.js
import { createStore } from 'redux'
import rootReducer from './reducer'

const store = createStore(rootReducer)

export default store
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/324a70a4-5dd4-1864-8d0f-67f944980ffa.png)

## 初期stateをロード

- `createStore`の第2引数`preloadedState`を用いて、store作成時の初期データを渡す
    - サーバーから送信されたHTMLページに含まれている値
    - `localStorage`に保持され、ページに再度アクセス時に参照される値
```
# preloadedStateの使用例
import { createStore } from 'redux'
import rootReducer from './reducer'

let preloadedState
const persistedTodosString = localStorage.getItem('todos')

if (persistedTodosString) {
  preloadedState = {
    todos: JSON.parse(persistedTodosString)
  }
}

const store = createStore(rootReducer, preloadedState)
```

## actionをdispatch

- `store.dispatch(action)`を呼び出すと、下記処理が行われる
    - storeが`rootReducer(state, action)`をコール
    - rootReducerにインポートされているsliceReducerをコール
        - `todosReducer(state.todos, action)`
    - storeに新しいstate値を保存
    - storeに登録されたリスナーコールバックをコール
        - リスナーからは、`store.getState()`をコールし、最新stateを参照
```src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import './api/server'

import store from './store'

// Log the initial state
console.log('Initial state: ', store.getState())
// {todos: [....], filters: {status, colors}}

// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
  console.log('State after dispatch: ', store.getState())
)

// Now, dispatch some actions

store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about actions' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about reducers' })
store.dispatch({ type: 'todos/todoAdded', payload: 'Learn about stores' })

store.dispatch({ type: 'todos/todoToggled', payload: 0 })
store.dispatch({ type: 'todos/todoToggled', payload: 1 })

store.dispatch({ type: 'filters/statusFilterChanged', payload: 'Active' })

store.dispatch({
  type: 'filters/colorFilterChanged',
  payload: { color: 'red', changeType: 'added' }
})

// Stop listening to state updates
unsubscribe()

// Dispatch one more action to see what happens

store.dispatch({ type: 'todos/todoAdded', payload: 'Try creating a store' })

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)
```

### 作成したstoreが機能するかテスト

- UIがなくても、ブラウザのデベロッパーツールで更新ロジックをテスト可能
- VS Codeのターミナルで、アプリを起動すると、ブラウザが開かれる
```
C:\kanban\todo>cd redux-fundamentals-example-app
C:\kanban\todo\redux-fundamentals-example-app>npm start
```
- ブラウザで、デベロッパーツールを開き(F12)、コンソールの出力を確認
    - 各actionがdispatchされたとき、Reduxのstateがどのように変化するか確認できる
    - `unsubscribe()`がコールされると、リスナーコールバックが削除される
        - それ以降の、action dispatchは、ログ出力されない
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2523098/21caa6d2-52a2-4648-71b2-6076b5fc115c.png)

## Storeの内部構造

- Redux Store実装コードのミニチュア版
```
function createStore(reducer, preloadedState) {
  let state = preloadedState
  const listeners = []

  function getState() {
    return state
  }

  function subscribe(listener) {
    listeners.push(listener)
    return function unsubscribe() {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    state = reducer(state, action)
    listeners.forEach(listener => listener())
  }

  dispatch({ type: '@@redux/INIT' })

  return { dispatch, subscribe, getState }
}
```

- storeには、現在のstateとreducer関数が存在
- `getState`は現在のstateを返す
- `subscribe`は、リスナーコールバックの配列を保持し、新しいコールバックを削除するための関数を返す
- `dispatch`は、reducerを呼び出し、stateを保存し、listenerを実行
- store起動時に、1つのactionをdispatchし、reducerをそのstateで初期化
- store APIは、内部に`{dispatch、subscribe、getState}`を持つオブジェクト
    - `getState`でstateを取得可能
    - state値を直接書き換えたり、配列の`sort()`関数を使ってはいけない
        - `array.sort()`は、既存の配列要素を書き換える(`mutate`)
```
const state = store.getState()
// ❌ Don't do this - it mutates the current state!
state.filters.status = 'Active'
```

## おわりに

Redux Storeの概念を深掘りし、実装を行いました。
次回も続きます。お楽しみに。

##### [次回] [Django+Reactで学ぶプログラミング基礎(35): Reduxチュートリアル(TodoアプリのStoreをカスタマイズ)](https://qiita.com/mingchun_zhao/items/10695a21192168994396)