新人エンジニアがReduxを使ってみた
こんにちは、株式会社デンソーDnote編集チームです。
今回は新人エンジニアである私が、弊社サービスのgarmitでも導入されている「Redux」を、TodoアプリのReactでの作成を通して初めて触れてみたので、そこで得た知識をまとめました。
まだまだ未熟者の視点で書かれた記事ですが、同じく初めてReduxに触れる方々の手助になれば幸いです!(今回はReduxがメインですので、Reactの説明は省きます)
Reduxとは
まず初めにReduxとは何かについて簡単に説明します。
ReduxとはReactなどで作成されたすべてのコンポーネントからアクセス可能なデータを一元管理することができる状態管理ライブラリです。
Reactではデータは親子関係のあるコンポーネントでしか受け渡しができません。そのためReduxを利用しない場合、以下の図のようにデータをバケツリレーしなければならず、開発の規模が大きくなればなるほどデータの受け渡しが煩雑になります。
また、階層が深くなったコンポーネントにデータを流してバグが発生した場合、どのコンポーネントで問題が起きたかを把握しにくくなります。
一方Reduxでデータを一元管理すれば、コンポーネントの親子関係に関わらず管理されているデータにアクセスすることができ、バケツリレー問題を解消することができます。
Reduxの仕組み
Reduxはストア・アクション・リデューサー・ディスパッチの4つの主要な要素で状態管理を実現しています。
以下では具体的にその4つの要素の役割を説明します。
ストア(Store):
Reduxでは、アプリケーション全体の状態が一つのストアに集約されます。このストアは、アプリケーションの全ての状態を保持し、アプリケーション内のすべてのコンポーネントがこのストアから状態を取得できます。アクション(Action):
アプリケーション内で何かしらのイベントが発生したとき、そのイベントに合わせた変更内容を示すオブジェクトがアクションです。アクションは、アプリケーションの状態がどのように変更されるかをストアに通知します。リデューサー(Reducer):
リデューサーは、アクションを受け取り、前の状態とともに新しい状態を返す関数です。リデューサーは、アプリケーションの状態変更のロジックを定義します。ディスパッチ(Dispatch):
アクションが発生したとき、そのアクションをストアに送信するプロセスをディスパッチと呼びます。ディスパッチによって、アクションがリデューサーに渡され、新しい状態が生成されます。
それぞれの要素の役割を踏まえた上で状態管理フローを整理すると以下のようになります。
ユーザーが何かしらの操作を行うと、対応するアクションを作成します。
アクションはディスパッチ(送信)され、リデューサーに渡されます。
リデューサーはアクションに基づいて前の状態を変更し、新しい状態を生成します。
リデューサーから生成された新しい状態がストアに保存されます。
ストアが更新されたことにより、関連するコンポーネントが再レンダリングされ、UIが更新されます。
Todoアプリ作成
では、Reduxの仕組みをざっと把握したうえでTodoアプリを作成していきます。
まず、プロジェクトを初期化します
npm install redux react-redux
npx create-react-app 任意のアプリケーション名
次に、Reduxをインストールします
npm install redux react-redux
アクションでTodoの状態変更の内容を、タスクの登録・完了・削除の3つを定義します。
「type」はそれぞれのActionを識別するための文字列です。
「payload」は状態変更に必要な任意のデータです。
todoActions.js
export const addTodo = (text) => ({
type: 'ADD_TODO',
payload: {
text,
},
});
export const toggleTodo = (id) => ({
type: 'TOGGLE_TODO',
payload: {
id,
},
});
export const deleteTodo = (id) => ({
type: 'DELETE_TODO',
payload: {
id,
},
});
次にリデューサーで、どのような変更内容でstateを返してほしいかを定義します。
「initialState」は、ToDoリストの初期状態を定義しています。
「todoReducer」はstateとActionを引数で受け取り新しい状態を返すための関数です。
関数内ではswitch文で、アクションで定義したtypeを識別子としてADD_TODO (Todoの追加)、TOGGLE_TODO (Todoの完了)、DELETE_TODO (Todoの削除)のそれぞれの処理を定義しています。
todoReducer.js
const initialState = {
todos: [],
};
const todoReducer = (state = initialState, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload.text,
completed: false,
},
],
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload.id ? { ...todo, completed: !todo.completed } : todo
),
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload.id),
};
default:
return state;
}
};
export default todoReducer;
以下ではルートリデューサーを作成しています。Reduxアプリケーションでは、異なる部分ごとに異なるReducerが存在することが一般的です。例えば、ToDoアプリケーションであれば、ToDoリストの状態を管理するためのReducerの他に、今回は作成していませんがユーザー情報を管理するためのReducerなどがあります。ルートリデューサーを作成することで、これらの状態を適切に組織化し、独立したReducerごとに担当させることができます。
index.js
import { combineReducers } from 'redux';
import todoReducer from './todoReducer';
const rootReducer = combineReducers({
todo: todoReducer,
});
export default rootReducer;
リデューサーの作成が完了したら次に、ストアを作成します。 createStore関数に先ほど作成したrootReducerを引数に入れます。
store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
const store = createStore(rootReducer);
export default store;
コンポーネントを定義します。
useSelectorのコールバックの引数はstateの現在の状態を表し、state.todo.todosとしてストアからtodos配列を取得しています。取得した配列はmap関数で一覧表示に用いています。
addTodo・toggleTodo・deleteTodo のActionをtodoActions.jsからimportし、それぞれのイベントでdispatch関数の引数としています。dispatch関数はReduxの仕組みで説明したとおり、アクションをリデューサーに渡し、新しい状態を生成するための関数です。
TodoApp.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, deleteTodo } from '../actions/todoActions';
const TodoApp = () => {
const todos = useSelector((state) => state.todo.todos);
const dispatch = useDispatch();
const [newTodo, setNewTodo] = useState('');
const handleAddTodo = () => {
if (newTodo.trim() !== '') {
dispatch(addTodo(newTodo));
setNewTodo('');
}
};
return (
<div>
<h1>ToDo App</h1>
<div>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button onClick={handleAddTodo}>Add Todo</button>
</div>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => dispatch(toggleTodo(todo.id))}
>
{todo.text}
</span>
<button onClick={() => dispatch(deleteTodo(todo.id))}>Delete</button>
</li>
))}
</ul>
</div>
);
};
export default TodoApp;
最後に「src」直下のindex.jsファイル内にTodoAppコンポーネントをimportし、ReduxからimportしたProviderコンポーネントでラップします。 rootコンポーネントに実装することで、それ以下の子コンポーネントにReduxの状態を共有することができます。
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import TodoApp from './components/TodoApp';
import './index.css';
ReactDOM.render(
<Provider store={store}>
<React.StrictMode>
<TodoApp />
</React.StrictMode>
</Provider>,
document.getElementById('root')
);
今回はTodoアプリの作成をもとに、Reduxに触れてみました。
Reduxによる状態の一元管理のメリットをより実感するためには大規模なコンポーネントツリーを用意する必要がありますが、そうでなくてもReduxのそれぞれの要素の役割や仕組みを理解するには小規模でも実際に手を動かしてみることが近道だと感じました。 今後もこのような技術のご紹介や時事ネタを取り扱っていきますので、引き続きよろしくお願いいたします!