Redux(Actions, Reducers, Store)による状態管理

Reduxによる状態管理の動作確認を行います。Actions, Reducers, Storeなどの役割を実際の処理を通じて確認します。

Reduxの3原則

動作確認をする前に、Reduxの3原則を復習しておきます。

https://redux.js.org/introduction/three-principlesにてReduxの3原則が説明されいています。

  1. Single source of truth
  2. State is read-only
  3. Changes are made with pure functions

Reducerは純粋関数でなければならないなどのReduxを利用する上での注意点が述べられています。

Reduxの動作確認用コード

Reduxのみの動作確認にReactは不要なので、nodeで動作確認します。

reduxをインストール

動作確認用のプロジェクトを構築します。

mkdir test-redux
cd test-redux/
npm init

reduxをインストールします。

npm install --save redux

動作確認用ソースのフォルダ構造

今回、以下フォルダ構造で動作確認用の処理を実装します。

├── redux
│   ├── count
│   │   ├── count.actions.js
│   │   ├── count.reducer.js
│   │   └── count.types.js
│   ├── log
│   │   ├── log.actions.js
│   │   ├── log.reducer.js
│   │   └── log.types.js
│   ├── root-reducer.js
│   └── store.js
├── app.js
├── package-lock.json
└── package.json

Action Typesを定義

redux/count/count.types.js

const CountActionTypes = {
  INCREMENT_COUNT: 'INCREMENT_COUNT',
  DECREMENT_COUNT: 'DECREMENT_COUNT',
}

export default CountActionTypes

redux/log/log.types.js

const LogActionTypes = {
  ADD_LOG: 'ADD_LOG',
  DELETE_LOG: 'DELETE_LOG',
  SET_LOG_LOADING: 'SET_LOG_LOADING',
}

export default LogActionTypes

Action Creatorsを定義
( actionオブジェクトを生成する関数 )

戻り値として actionオブジェクト(ユーザが行った操作を表現) を返す関数を定義します。

{
    type: "アクションの種類",
    payload: "アクション実行に利用するデータ",
}

redux/count/count.actions.js

import CountActionTypes from './count.types'

export const incrementCount = () => {
  return ({
    type: CountActionTypes.INCREMENT_COUNT,
  })
}
export const decrementCount = () => {
  return ({
    type: CountActionTypes.DECREMENT_COUNT,
  })
}

redux/log/log.actions.js

import LogActionTypes from './log.types'

export const addLog = (id, text) => {
  return ({
    type: LogActionTypes.ADD_LOG,
    payload: {
      id,
      text
    },
  })
}
export const deleteLog = id => {
  return ({
    type: LogActionTypes.DELETE_LOG,
    payload: {
      id
    },
  })
}
export const setLogLoading = () => {
  return {
    type: LogActionTypes.SET_LOG_LOADING,
  }
}

Reducersを定義
( 状態を変更する純粋関数 )

引数として、stateオブジェクト(変化前の状態)actionオブジェクト を受け取ります。
戻り値として、 stateオブジェクト(変化後の状態) を返します。

redux/count/count.reducer.js

import CountActionTypes from './count.types'

const initialState = 0

const counterReducer = (state = initialState, action) => {
  switch (action.type) {
    case CountActionTypes.INCREMENT_COUNT:
      return state + 1
    case CountActionTypes.DECREMENT_COUNT:
      return state - 1
    default:
      return state
  }
}

export default counterReducer

redux/log/log.reducer.js

import LogActionTypes from './log.types'

const initialState = {
  logs: [],
  loading: false,
}

const logReducer = (state = initialState, action) => {
  switch (action.type) {
    case LogActionTypes.ADD_LOG:
      return {
        ...state,
        logs: [...state.logs, action.payload],
        loading: false
      }
    case LogActionTypes.DELETE_LOG:
      return {
        ...state,
        logs: state.logs.filter(log => log.id !== action.payload.id),
        loading: false
      }
    case LogActionTypes.SET_LOG_LOADING:
      return {
        ...state,
        loading: true
      }
    default:
      return state
  }
}

export default logReducer

Reducersを1つにまとめる
( redux.combineReducers )

redux/root-reducer.js

import { combineReducers } from 'redux'

import counterReducer from './count/count.reducer'
import logReducer from './log/log.reducer'

const rootReducer = combineReducers({
  log: logReducer,
  counter: counterReducer
})

export default rootReducer

Storeを生成
( redux.createStore )

redux/store.js

import { createStore } from 'redux'

import rootReducer from './root-reducer'

const initialState = {}
const store = createStore(rootReducer, initialState)

export default store

動作確認用処理
- 状態取得: store.getState()
- 状態更新: store.dispatch()

以下動作を確認するための処理を実装します。

  • store.getState() で状態取得できること
  • store.dispatch(actionオブジェクト) で状態更新できること

app.js

import { incrementCount, decrementCount } from './redux/count/count.actions'
import { addLog, deleteLog, setLogLoading } from './redux/log/log.actions'
import store from './redux/store'

console.log('############ state(初期値) ############')

console.log(JSON.stringify(store.getState()))
console.log()

console.log('############ 動作確認(Counter state) ############')

store.dispatch(incrementCount())
console.log(JSON.stringify(store.getState()))

store.dispatch(incrementCount())
console.log(JSON.stringify(store.getState()))

store.dispatch(decrementCount())
console.log(JSON.stringify(store.getState()))
console.log()

console.log('############ 動作確認(Log state) ############')

store.dispatch(setLogLoading())
console.log(JSON.stringify(store.getState()))

store.dispatch(addLog(1, 'x'))
console.log(JSON.stringify(store.getState()))

store.dispatch(setLogLoading())
console.log(JSON.stringify(store.getState()))

store.dispatch(addLog(2, 'y'))
console.log(JSON.stringify(store.getState()))

store.dispatch(setLogLoading())
console.log(JSON.stringify(store.getState()))

store.dispatch(addLog(3, 'z'))
console.log(JSON.stringify(store.getState()))

store.dispatch(setLogLoading())
console.log(JSON.stringify(store.getState()))

store.dispatch(deleteLog(2))
console.log(JSON.stringify(store.getState()))

実行

前準備|babel導入

import などnodeでは利用できないので、babelを導入しておきます。

npm install --save-dev \
@babel/cli \
@babel/core \
@babel/node \
@babel/plugin-proposal-object-rest-spread \
@babel/preset-env

.babelrc ファイルを作成して以下内容を記述します。

{
  "presets": [
    "@babel/preset-env"
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread"
  ]
}

実行結果

$ npx babel-node app.js 
############ state(初期値) ############
{"log":{"logs":[],"loading":false},"counter":0}

############ 動作確認(Counter state) ############
{"log":{"logs":[],"loading":false},"counter":1}
{"log":{"logs":[],"loading":false},"counter":2}
{"log":{"logs":[],"loading":false},"counter":1}

############ 動作確認(Log state) ############
{"log":{"logs":[],"loading":true},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"}],"loading":false},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"}],"loading":true},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"},{"id":2,"text":"y"}],"loading":false},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"},{"id":2,"text":"y"}],"loading":true},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"},{"id":2,"text":"y"},{"id":3,"text":"z"}],"loading":false},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"},{"id":2,"text":"y"},{"id":3,"text":"z"}],"loading":true},"counter":1}
{"log":{"logs":[{"id":1,"text":"x"},{"id":3,"text":"z"}],"loading":false},"counter":1}

補足|イメージ図

動作確認に利用した処理のイメージ図を描いてみました。

703-react-redux_image.png

参考