環境は
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')
)

Providerstoreを読み込んで、Appコンポーネントを囲む部分を追記しています。

これで Redux Toolkit の準備は完了です。
ここまでは基本そのまま使う部分だと思いますので、こういうものだと思って進めていきましょう。

createSlice

createSliceという機能を使うことで、StateReducerActionを一気に作成することができます。

今回は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-reduxuseSelectorを使います。
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-reduxuseDispatchdispatchを使えるようにしましょう。
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コンポーネントの関数をdoneTaskdeleteTaskアクションに変更します。
やってることは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)版はこちら!