環境は
react : 16.12
typescript : 3.7
@reduxjs/toolkit : 1.2
react-redux : 7.1
でお送りします。
この記事でやること
基本的に以前作成したTodoアプリにRedux Toolkitを導入します。
Todoアプリについては下記記事を参照ください。
React.js + TypeScript でTodoアプリを作ってみる
なぜReduxを使うのか
前回作成したTodoアプリの規模のアプリケーションであれば、このままでも問題はないのですが、コンポーネントが増えるとデータを次々とPropsで渡すということにり管理が難しくなります。
またステートを変更する関数はリストに作成しましたが、ここでよかったのかという問題もあります。
この辺りを解決する手段としてReduxというライブラリがあります。
Reduxのフロー図
かなりざっくりなのですが、Reduxのステート更新の流れです。
View(コンポーネント)からActionを実行して、Reducerを通してStateが更新され、Viewに反映されます。
Reduxで作る場合はこの処理を一つ一つ書いていく必要があるのですが、Redux Toolkit を使用することで、基本的な流れは変わりませんが、ある程度処理をまとめて書くことができるというイメージです。

ライブラリのインストール
最初にライブラリのインストールをしましょう。
@reduxjs/toolkitの他にreact-reduxと、TypeScriptの場合@types/react-reduxが必要です。
$ npm install --save @reduxjs/toolkit react-redux @types/react-redux
Redux Toolkit の準備
Redux Toolkit を使う為の準備をしていきます。
rootReducer.tsファイルを下記のように新規作成します。
src/rootReducer.ts
import { combineReducers } from '@reduxjs/toolkit'
const rootReducer = combineReducers({})
export type RootState = ReturnType<typeof rootReducer>
export default rootReducer
combineReducersは今は空ですが、後ほど作成するReducerを入れることになります。
type RootStateはステートの型指定する時に使います。
次はstore.tsです。
configureStoreに先ほど作成したrootReducerを登録してstoreとして使えるようにします。
src/store.ts
import { configureStore } from '@reduxjs/toolkit'
import rootReducer from './rootReducer'
const store = configureStore({
reducer: rootReducer
})
export type AppDispatch = typeof store.dispatch
export default store
次に共通のストアを各コンポーネントで使えるようにする必要があります。
トップコンポーネントに設定するので、index.tsxを次のように編集しましょう。
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux'
import store from './store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
Providerとstoreを読み込んで、Appコンポーネントを囲む部分を追記しています。
これで Redux Toolkit の準備は完了です。
ここまでは基本そのまま使う部分だと思いますので、こういうものだと思って進めていきましょう。
createSlice
createSliceという機能を使うことで、State、Reducer、Actionを一気に作成することができます。
今回はmodulesというディレクトリを作りその中にtasksModule.tsファイルを作成しました。
src/modules/tasksModule.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { Task } from '../Types'
type State = {
count: number
tasks: Task[]
}
const initialState: State = {
count: 2,
tasks: [
{
id: 2,
title: '次のTodo',
done: false
},{
id: 1,
title: '最初のTodo',
done: true
}
]
}
const tasksModule = createSlice({
name: 'tasks',
initialState,
reducers: {
addTask (state: State, action: PayloadAction<string>) {
state.count++
const newTask: Task = {
id: state.count,
title: action.payload,
done: false
}
state.tasks = [newTask, ...state.tasks]
},
doneTask (state: State, action: PayloadAction<Task>) {
const task = state.tasks.find(t => t.id === action.payload.id)
if (task) {
task.done = !task.done
}
},
deleteTask (state: State, action: PayloadAction<Task>) {
state.tasks = state.tasks.filter(t =>
t.id !== action.payload.id
)
}
}
})
export const {
addTask, doneTask, deleteTask
} = tasksModule.actions
export default tasksModule
- name
- このcreateSliceを識別するための名前を付けます。
- initialState
-
ステートの初期データを入れます。
ここではあらかじめ作成したinitialStateをそのまま入れてます。 - reducers
-
ステートを変更する為の処理はここに書きます。
各コンポーネントで書いていた関数をこちらに移動しましょう。
ここで設定する関数は、第一引数にstateを受け取り、実行時に渡した引数は第二引数にactionとして受け取ります。実際の値はaction.payloadで取り出します。
設定したReducerはtasksModule.actions.addTask()で実行できます。
コンポーネントで使いやすいようにtasksModule.actionsでエクスポートしておきましょう。
このようにすることでコンポーネントからaddTask()だけで実行できるようになります。
作成したら先ほど空だったrootReducerに入れておきましょう。
src/rootReducer.ts
import tasksModule from './modules/tasksModule'
const rootReducer = combineReducers({
tasks: tasksModule.reducer
})
コンポーネントからステートにアクセスする
createSliceで作成したステートにアクセスするには、react-reduxのuseSelectorを使います。
TaskListコンポーネントのtasksを置き換えてみましょう。
この部分はPropsで受け取っていましたので、Propsは必要なくなりました。
src/components/TaskList.tsx
import React from 'react'
import TaskItem from './TaskItem'
import { useSelector } from 'react-redux'
import { RootState } from '../rootReducer'
const TaskList: React.FC = () => {
const { tasks } = useSelector((state: RootState) => state.tasks)
return (
<div className="inner">
{
tasks.length <= 0 ? '登録されたTODOはありません。' :
<ul className="task-list">
{ tasks.map(task => (
<TaskItem key={task.id} task={task} />
)) }
</ul>
}
</div>
)
}
export default TaskList
コンポーネントの関数をActionに置き換える
次はコンポーネントの書いていた関数をcreateSliceで作成したActionに置き換えます。
タスクの登録処理を書いていたTaskInputから編集します。
Actionはdispatchの引数に入れることで実行できます。
react-reduxのuseDispatchでdispatchを使えるようにしましょう。
handleSubmitの処理の部分はReducerに移したので、その部分をdispatch(addTask(inputTitle))に変更します。
src/components/TaskInput.tsx
import React, { useState } from 'react'
import { useDispatch } from 'react-redux'
import { addTask } from '../modules/tasksModule'
const TaskInput: React.FC = () => {
const dispatch = useDispatch()
const [ inputTitle, setInputTitle ] = useState('')
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputTitle(e.target.value)
}
const handleSubmit = () => {
dispatch(addTask(inputTitle))
setInputTitle('')
}
return (
<div className="input-form">
<div className="inner">
<input
type="text"
className="input"
value={inputTitle}
onChange={handleInputChange}
placeholder="TODOを入力してください。"
/>
<button onClick={handleSubmit} className="btn is-primary">追加</button>
</div>
</div>
)
}
export default TaskInput
次はTaskItemコンポーネントの関数をdoneTaskとdeleteTaskアクションに変更します。
やってることはTaskInputとほとんど変わらないですね。
src/components/TaskItem.tsx
import React from 'react'
import { Task } from '../Types'
import { useDispatch } from 'react-redux'
import { doneTask, deleteTask } from '../modules/tasksModule'
type Props = {
task: Task
}
const TaskItem: React.FC<Props> = ({ task }) => {
const dispatch = useDispatch()
return (
<li className={task.done ? 'done' : ''}>
<label>
<input
type="checkbox"
className="checkbox-input"
onClick={() => dispatch(doneTask(task))}
defaultChecked={ task.done }
/>
<span className="checkbox-label">{ task.title }</span>
</label>
<button
onClick={() => dispatch(deleteTask(task))}
className="btn is-delete"
>削除</button>
</li>
)
}
export default TaskItem
Propsが必要なくなったのでApp.tsxも編集しておきましょう。
src/App.tsx
import React from 'react'
import TaskList from './components/TaskList'
import TaskInput from './components/TaskInput'
import './App.css'
const App: React.FC = () => {
return (
<div>
<TaskInput />
<TaskList />
</div>
)
}
export default App
以上です。Reduxで挫折した人もRedux Toolkitなら入りやすいのではないでしょうか。
Advanced Tutorial: Redux Toolkit in Practice
Redux の記述量多すぎなので、 Redux の公式ツールでとことん楽をする。 ( Redux Toolkit)
この記事の動画(Youtube)版はこちら!
