はじめに
React を習得するまでの軌跡をメモっていく備忘録的な記事です。
Redux Toolkit を使ってみる
2019年にリリースされた React の状態管理のライブラリで従来の Redux をより使いやすくしたものらしい。
Redux Toolkit の公式サイト
公式サイトの Intermediate Tutorial を参考にしました。
サンプルコードのGitHubのリポジトリはこちら
サンプルコードの中身は todo アプリで
仕様としては
- 一個一個の todo は text(すべきこと) と completed(完了したかどうか) を持つ
- フッターのボタンで all(全てのTODOを表示), active(完了してないTODOを表示) , completed(完了したTODOを表示) を切り替えることができる
UIは以下のような感じです。
実際にソースコードを読む
ファイル構成はこんな感じです。
Redux Toolkitの要になりそうな部分だけ取り上げています。
ソースコード中にコメントを入れる形で書いていきます。
index.js
import React from 'react'
import { render } from 'react-dom'
import { configureStore } from '@reduxjs/toolkit'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = configureStore({
// store を定義する
// rootReducer は createSliceで作った Reducer 達を combineReducerにより合体させたもの
// (↑ reducers/index.js で定義している)
reducer: rootReducer
})
render(
// 子コンポーネントが store に接続できるように
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
TODOを追加したり、完了状態を変更する部分の実装
features/todos/todoSlice.js
import { createSlice } from '@reduxjs/toolkit' // createSlice を使えるようにする
// slice を作成する Redux Toolkitのメイン部分
// State, Reducer, Action を一気に生成する
const todosSlice = createSlice({
name: 'todos', // slice の名前を設定
initialState: [], // state の初期値を設定
reducers: { // reducer を設定 複数の reducer を設定できる
// prepare のコールバックを用いた書き方
addTodo: {
reducer(state, action) {
const { id, text } = action.payload // action.payload には reducer に渡したい正味の値が入る
state.push({ id, text, completed: false }) // state に新規の値を追加している
},
//prepare のコールバックを使うとstateを更新する前に色々と事前に編集できて便利
prepare(text) {
return { payload: { text, id: nextTodoId++ } }
}
},
// こっちが通常の書き方
toggleTodo(state, action) {
// state の中から id に一致した todo を見つける
const todo = state.find(todo => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed // 見つけた todo の complete を更新する
}
}
}
})
// addTodo と toggleTodo の Action を生成できる関数を export
export const { addTodo, toggleTodo } = todosSlice.actions
// Reducer を export
export default todosSlice.reducer
connect, mapDispatch, mapStateToProps についてはこちらの記事が大変参考になりました。
mapStateToPropsとmapDispatchToPropsの理解の仕方
features/todos/AddTodo.js
import React, { useState } from 'react'
import { connect } from 'react-redux'
import { addTodo } from './todosSlice'
// AddTodo component に props として addTodo の Action を渡すため
const mapDispatch = { addTodo }
const AddTodo = ({ addTodo }) => {
const [todoText, setTodoText] = useState('')
const onChange = e => setTodoText(e.target.value)
return (
<div>
<form
{
// 送信ボタンが押された時、form の値がなければそのまま return
// 値があれば addTodoの Action が Reducer にフォームの値を送り、 state が更新される、
// その後、フォームは空になる
}
onSubmit={e => {
e.preventDefault()
if (!todoText.trim()) {
return
}
addTodo(todoText)
setTodoText('')
}}
>
<input value={todoText} onChange={onChange} />
<button type="submit">Add Todo</button>
</form>
</div>
)
}
// connectを使って Action を AddTodo に Propsとして送る
export default connect(
null,
mapDispatch
)(AddTodo)
表示するTODOの切り替え部分の実装
features/filters/filterSlice.js
import { createSlice } from '@reduxjs/toolkit' // createSlice を使えるようにする
// フィルタリングの状態(全て、完了、未完了)を定義した定数
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
const filtersSlice = createSlice({
name: 'visibilityFilters', // slice の名前を設定
initialState: VisibilityFilters.SHOW_ALL, // slice の初期値を設定
reducers: { // コールバックは使わず通常の書き方
setVisibilityFilter(state, action) {
return action.payload // 受け取った action の値でそのまま state を更新
}
}
})
// Action を export
export const { setVisibilityFilter } = filtersSlice.actions
// Reducer を export
export default filtersSlice.reducer
features/filters/Link.js
import React from 'react'
import PropTypes from 'prop-types'
// filter には VisibilityFilters のいずれかが入る
const Link = ({ active, children, setVisibilityFilter, filter }) => (
<button
// setVisibilityFilter Action で Reducer に filter の値を送信
onClick={() => setVisibilityFilter(filter)}
disabled={active} // 現状クリックされているなら disabled に
style={{
marginLeft: '4px'
}}
>
{children}
</button>
)
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
setVisibilityFilter: PropTypes.func.isRequired,
filter: PropTypes.string.isRequired
}
export default Link
Storeから State を受け取り表示する部分の実装
src/features/todos/VisibleTodoList.js
import { connect } from 'react-redux'
import { createSelector } from '@reduxjs/toolkit'
import { toggleTodo } from 'features/todos/todosSlice'
import TodoList from './TodoList'
import { VisibilityFilters } from 'features/filters/filtersSlice'
const selectTodos = state => state.todos // Store から todos の state を select
const selectFilter = state => state.visibilityFilter // Store から visibilityFilter の state を select
const selectVisibleTodos = createSelector( // selector を作成
[selectTodos, selectFilter],
(todos, filter) => {
switch (filter) {
case VisibilityFilters.SHOW_ALL:
return todos
case VisibilityFilters.SHOW_COMPLETED:
return todos.filter(t => t.completed)
case VisibilityFilters.SHOW_ACTIVE:
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
)
const mapStateToProps = state => ({
todos: selectVisibleTodos(state)
})
const mapDispatchToProps = { toggleTodo }
export default connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
まとめ
-
createSlice
を使うとState, Action, Reducer
を一気に作れるので便利。 -
Component
にprops
としてState
やAction
を渡したい場合は、
connect, mapStateToProps, mapDispatchToProps
を使うと便利。 -
Reducer
をcombineReducer
で結合させてrootReducer
を作る -
configureStore
でstore
を定義する。 - 作った
store
はProvider
経由で 子コンポーネントに渡す。