環境は
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)版はこちら!