はじめに
これまでContext APIやZustandを使って状態の一元管理を行った経験はありますが、Reduxは利用したことがなかったため、今回試してみます。
状態管理とは?
状態管理とは、データ(状態)をコンポーネント内またはコンポーネント間で更新するプロセスのことを指します。
Reactでは標準で利用できるContext APIを使ってグローバルステートが使えます。
ローカルステートとグローバルステート
状態管理には、ローカルステートとグローバルステートの2種類があります。
- ローカルステート:コンポーネント内のみで状態管理ができるものです。他コンポーネントには影響しません。
状態管理のライブラリを導入していない場合は、useStateを使用します。 - グローバルステート:コンポーネント間で使用できる状態管理のこと。グローバルステートを使用することで、propsのバケツリレーが不要になります。
状態管理のライブラリを導入していない場合でも、useContextを使用することでグローバルステートの管理が可能です。
状態管理のライブラリは何がある?
主な状態管理ライブラリには、以下のものがあります。
- Redux
- Recoil
- Zustand
- Jotai
- Valtio
npm trendsで比較するとReduxが圧倒的に採用率が高いようです。
ライブラリの選定はプロダクトにより最適なものが異なると思いますが、今回はReduxを使用するため、ReduxとContext APIの使用シーンを比較してみました。
状態管理 | 適した使用シーン |
---|---|
Context API |
・小規模アプリケーション向け ・状態が単純な場合(テーマの切り替え、認証情報の共有など) ・非同期処理があまりない ・学習コスト:低 |
Redux |
・中~大規模アプリケーション向け ・状態の依存関係が複雑、または管理する状態が多い場合 ・デバッグツールや非同期処理のサポートが重要な場合 ・学習コスト:高 |
Reduxの状態管理を使ってTODOアプリを作ってみる
Reduxに適したシーンは概ね理解できたので、ReactとReduxを組み合わせてアプリケーションを作成しながら学んでいきます。
1. 下準備
環境を準備するため、以下の手順でReactとTypeScriptの環境を構築します。
$ npm create vite@latest
> npx
> create-vite
√ Project name: ... redux-todo
√ Select a framework: » React
√ Select a variant: » TypeScript
次に、開発環境が正常に起動するか確認します。
pnpmがインストールされていない場合は、npmまたはyarnを使用してインストールと動作確認を行ってください。
$ pnpm i
...
$ pnpm dev
次に、デフォルトで記載されているsrc/App.tsxとsrc/main.tsxを以下のように書き換えます。
function App() {
return (
<div>
<h1>Todoリスト</h1>
</div>
);
}
export default App;
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>
);
次に、Redux 関連のライブラリをインストールします。
Reduxのハンズオン系の記事では、reduxとreact-reduxをインストールするような記述を見かけるかと思いますが、後述するStoreの作成時に使用するcreateStoreは現在非推奨となっています。
公式の推奨方法である @reduxjs/toolkit をインストールして対応します。
$ pnpm i @reduxjs/toolkit react-redux
以上で、下準備は完了です!
2. Storeを作成
srcディレクトリ直下にstoreディレクトリを作成し、index.tsファイルを作成します。
index.ts内は以下を追加します。
import { configureStore } from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {},
});
3. Providerコンポーネントの設定
storeからすべてのコンポーネントからアクセスできるように src/main.tsx を更新します。
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App.tsx";
import { Provider } from "react-redux";
import { store } from "./store/index.ts";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>
);
ここではreact-reduxからインポートしたProviderでAppコンポーネントをラップしています。
さらに作成したstoreをpropsとしてProviderに渡しています。
4. Sliceファイルを追加
storeディレクトリ内にtodoSliceファイルを作成します。
import { createSlice } from "@reduxjs/toolkit";
interface Todo {
name: string;
complete: boolean;
}
interface TodoState {
lists: Todo[];
}
const initialState: TodoState = {
lists: [
{ name: "朝食を作る!", complete: false },
{ name: "昼食を作る!", complete: false },
],
};
export const todoSlice = createSlice({
name: "todos",
initialState,
reducers: {},
});
export default todoSlice.reducer;
ここでは「TODOリスト」の状態と、それを操作するための機能を定義しています。
- name: todo
- このスライスの名前を定義しています。Reduxで生成されるアクションのタイプやデバッグ時にこの名前が利用されます。
- initialState
- 初期状態を定義しています。ここでは初期状態として「朝食を作る!」と「昼食を作る!」というTODOタスクが設定しています。
- reducers: {}
- 状態を更新するための関数(リデューサー)を定義するためのプロパティです。
- export default todoSlice.reducer
- 最後にtodoSlice.reducerをエクスポートしています。スライスを作成すると、createSlice によってリデューサーが自動生成されます。このリデューサーが、状態を変更する際に実行される関数として動作します。
5. store/index.tsにtodoReducerを追加
todoSliceを作成したら、store/index.tsにtodoSliceを追加します。
import { configureStore } from "@reduxjs/toolkit";
import todoReducer from "./todoSlice";
export const store = configureStore({
reducer: {
todos: todoReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
リデューサーをインポートする場合、import todoReducer from "./todoSlice";
という点に気をつけてください。
ついでに呼び出した際の状態全体の型の定義を追加しています。
6. AppへTodoリストを呼び出す
storeのindex.tsにtodoSliceを追加したら src/App.tsx にTodoリストを表示させます。
import { useSelector } from "react-redux";
import { RootState } from "./store";
function App() {
const lists = useSelector((state: RootState) => state.todos.lists);
return (
<div>
<h1>Todoリスト</h1>
<ul>
{lists.map((list, index) => (
<li key={index}>{list.name}</li>
))}
</ul>
</div>
);
}
export default App;
以下のように初期状態のリストが表示されていれば成功です!
7. リスト分け
取得したlistsに対してfilter関数を実行して未完了と完了に分けます。
import { useSelector } from "react-redux";
import { RootState } from "./store";
function App() {
const lists = useSelector((state: RootState) => state.todos.lists);
return (
<div>
<h1>Todoリスト</h1>
<h2>未完了</h2>
<ul>
{lists
.filter((list) => list.complete === false)
.map((list, index) => (
<li key={index}>{list.name}</li>
))}
</ul>
<h2>完了</h2>
<ul>
{lists
.filter((list) => list.complete === true)
.map((list, index) => (
<li key={index}>{list.name}</li>
))}
</ul>
</div>
);
}
export default App;
8. リストの移動
最後に未完了から完了へのリスト移動をできるようにします。
まずはtodoSliceのreducersを追加します。
import { createSlice } from "@reduxjs/toolkit";
interface Todo {
name: string;
complete: boolean;
}
interface TodoState {
lists: Todo[];
}
const initialState: TodoState = {
lists: [
{ name: "朝食を作る!", complete: false },
{ name: "昼食を作る!", complete: false },
],
};
export const todoSlice = createSlice({
name: "todos",
initialState,
reducers: {
doneList: (state, action) => {
const { name } = action.payload;
const item = state.lists.find((item) => item.name === name);
if (item) {
item.complete = true;
}
},
},
});
export const { doneList } = todoSlice.actions;
export default todoSlice.reducer;
次にAppに完了ボタンを追加します。
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "./store";
import { doneList } from "./store/todoSlice";
function App() {
const lists = useSelector((state: RootState) => state.todos.lists);
const dispatch = useDispatch();
return (
<div>
<h1>Todoリスト</h1>
<h2>未完了</h2>
<ul>
{lists
.filter((list) => list.complete === false)
.map((list, index) => (
<li key={index}>
{list.name}
<button onClick={() => dispatch(doneList({ name: list.name }))}>
完了
</button>
</li>
))}
</ul>
<h2>完了</h2>
<ul>
{lists
.filter((list) => list.complete === true)
.map((list, index) => (
<li key={index}>{list.name}</li>
))}
</ul>
</div>
);
}
export default App;
完成
以下のように「完了」ボタンクリック後、完了へ移動すれば完成です!
今回はReduxの状態をキャッチアップするために、未完了⇒完了への移動のみでしたが必要に応じてTodo追加、削除などもチャレンジしてみてください!
さいごに
React習得当初は、「Reduxは学習コストが高い」と感じてなかなか手をつけられずにいましたが、基本的な部分は意外とスムーズに理解できたかなと思います。
とはいえ、中~大規模のアプリにReduxを導入したプロジェクトへ参画することがあれば、壁にぶつかる予感がプンプンします。なので今後も定期的にキャッチアップしておきたいと思います...笑