WEBOPIXEL

Redux Toolkit で React.js の状態管理をもっと簡単にする方法[TypeScript版]

Posted: 2020.01.27 / Category: javascript / Tag: ,

React.jsの状態管理ライブラリはReduxが有名ですが、結構複雑だったりステートを一つ更新するだけでもタイプ量が多くなってしまうというデメリットもあります。
そこでこの記事ではRedux Toolkitというライブラリを使用して、もう少し簡単に状態管理する方法をご紹介します。

Sponsored Link

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