目次
なぜReduxなのか
Part4.1や、Part4.2でProps、useContextを学びました。
いずれも表示値や制御値をどこで管理するかということになります。
propsの欠点は、バケツリレー。
バケツリレーをもう少し説明すると、
- 親、子、孫、ひ孫・・・・というコンポーネント階層があり
- 親で状態管理する(変数を表示する)要素があり、ひ孫でそれを更新する処理がある。逆も然り。
このような状況化で、propsで状態を受け渡すのは煩雑であり、また更新するたびに連なっているコンポーネントがレンダリングされることになりパフォーマンス的にも良くない。
(値を受け渡すだけで使用していないコンポーネントでもレンダリングが走るようです。またその対策もあるようですが、propsの中身がオブジェクトの場合はその対策も効かないようです。)
以下のようなイメージです。※こちらからイメージ画像いただきました。
propsの場合、孫①で更新された内容を子①、親、子②を通じて孫③へ伝達します。
Reduxの場合、孫①はStore上の値を更新し、孫③はStore上の値を読み取ります。
Reduxは、バケツリレーせずにグローバル値のように値を管理できるということになります。
よく状態管理という言葉が使われますが、私はなんだか馴染めない。。
(教えてください。。)
Redux、Redux Toolkitについてちょっとだけ
タイトルにRedux編と書いてますが、
- Reduxは、使い易い感じはするが、ソースが多いので導入時のハードルが高い
- Redux ToolkitはReduxよりも導入が容易
- 2.の為、Reduxより主流になるのではという人もいます。
ということで、Redux Toolkitを導入してみることにしました。
ReduxではStoreという概念で状態管理を行いますが、Redux Toolkitでは、Slice
という概念があるようです。(Sliceという概念の中にStoreを管理してる?)
Reduxは家族を纏めて管理しているが、Redux Toolkitはお父さん、お母さん、長男などを別々に管理している感じとどこかに書いてありました。
※どこかは忘れました。
「State + Actions + Reducer」のセットがsliceとのことです。
Actions、Reducerなどはググってみてください(難しく説明できません。。。雰囲気くらいつかんでおけばいいかと。。。)
React + Typescript環境を作成
以下コマンドを実行します。
npx create-react-app プロジェクト名 --template redux-typescript
以下コマンドを実行し初期画面が表示されることを確認します。
cd プロジェクト名
yarn start
初期画面です。
※下記画面が表示されない場合、ブラウザでlocalhost:3000
にアクセスしてみてください。
Todoアプリ用のフォルダとファイルを作成します。
featuresディレクトリの下にtodo
ディレクトリを作成し、その下に以下のファイルを作成します。
- Todo.ts
- TodoInput.tsx
- TodoItem.tsx
- TodoList.tsx
- todoSlice.ts
Todo.ts
Todoの型を定義します。
export type Todo = {
id: number;
title: string;
done: boolean;
};
todoSlice.ts
Todoに関するAction、State、Reducerを定義します。
redux(-toolkit)の根幹でしょう。
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { Todo } from "./Todo";
// Todo管理の型定義
type State = {
count: number;
todos: Todo[];
};
// Todoの初期データ
const initialState: State = {
count: 3,
todos: [
{
id: 3,
title: "神を切る",
done: false,
},
{
id: 2,
title: "葉を洗う",
done: false,
},
{
id: 1,
title: "顔を磨く",
done: true,
},
],
};
// createSlice
export const todoSlice = createSlice({
name: "todo",
initialState,
reducers: {
addTodo(state: State, action: PayloadAction<string>) {
state.count++;
const newTodo: Todo = {
id: state.count,
title: action.payload,
done: false,
};
// 既存タスクに追加
state.todos = [newTodo, ...state.todos];
},
doneTodo(state: State, action: PayloadAction<Todo>) {
// 該当タスクを探し、doneフラグを更新する
const todo = state.todos.find((t) => t.id === action.payload.id);
if (todo) {
todo.done = !todo.done;
}
},
deleteTodo(state: State, action: PayloadAction<Todo>) {
// 該当タスクを除くタスクを、全タスクとする
state.todos = state.todos.filter((t) => t.id !== action.payload.id);
},
},
});
export const { addTodo, doneTodo, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
app/store.tsを修正
既存のstore.tsに先ほど書いたtodoSlice
を利用できるように追加します。
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
import todoReducer from "../features/todo/todoSlice"; // 追加
export const store = configureStore({
reducer: {
counter: counterReducer,
todo: todoReducer, // 追加
},
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
npx create-react-app プロジェクト名 --template typescript
TodoItem.tsx
Todo1アイテムを表示するコンポーネントを書きます。
- todoのdone状態を変更するチェックボックスを持ちます。
- todoのタイトルを表示します。
- 削除ボタンを持ちます。
- チェックボックスは、done状態を変更するActionをDispatchします。
- 削除ボタンは、タスクを削除するActionをDispatchします。
import { Todo } from "./Todo";
import { useDispatch } from "react-redux";
import { doneTodo, deleteTodo } from "./todoSlice";
type Props = {
todo: Todo;
};
const TodoItem: React.FC<Props> = ({ todo }) => {
const dispatch = useDispatch();
return (
<li>
<label>
<input
type="checkbox"
onClick={() => dispatch(doneTodo(todo))}
defaultChecked={todo.done}
/>
<span className="checkbox-label">{todo.title}</span>
</label>
<button onClick={() => dispatch(deleteTodo(todo))}>削除</button>
</li>
);
};
export default TodoItem;
TodoList.tsx
現在保持しているtodoを全て表示するコンポーネントを書きます。
import React from "react";
import TodoItem from "./TodoItem";
import { useSelector } from "react-redux";
import { RootState } from "../../app/store";
import { Todo } from "./Todo";
const TodoList: React.FC = () => {
const { todos } = useSelector((state: RootState) => state.todo);
return (
<div>
{todos.length <= 0 ? (
"登録されたTODOはありません。"
) : (
<ul>
{todos.map((todo: Todo) => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
)}
</div>
);
};
export default TodoList;
TodoInput.tsx
todoを新規追加するコンポーネントを書きます。
- todoタイトルを入力するテキストボックスがあります。
- todoを追加するボタンがあり、todoを追加するActionをDispatchします。
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { addTodo } from "./todoSlice";
const TodoInput: React.FC = () => {
const dispatch = useDispatch();
const [inputTitle, setInputTitle] = useState("");
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputTitle(e.target.value);
};
const handleSubmit = () => {
dispatch(addTodo(inputTitle));
setInputTitle("");
};
return (
<div>
<input
type="text"
value={inputTitle}
onChange={handleInputChange}
placeholder="TODOを入力してください。"
/>
<button onClick={handleSubmit}>追加</button>
</div>
);
};
export default TodoInput;
App.tsxを書きに書き換える
作成したコンポーネントを読み込むように設定します。
import React from "react";
import TodoList from "./features/todo/TodoList";
import TodoInput from "./features/todo/TodoInput";
import "./App.css";
const App: React.FC = () => {
return (
<div>
<TodoInput />
<TodoList />
</div>
);
};
export default App;
実食
以下のような画面になると思います。
追加、削除やチェックボックスの動作確認をしてみてください。
ちょっと休憩
用語の意味をちょっとだけ考えてみましょう。
◆reducer
〔+目的語+to+(代)名詞〕〈ものを〉(整理して)〔簡単な形に〕変える,まとめる.
⇒「状態とアクションを受け取って、次の状態を返すやつ」
◆action
アクションのタイプ(必須)とstate(値)のみ持つプレーンなオブジェクト
以下のようなイメージになります。
const action = {
type: 'ADD_TODO',
text: 'Go to swimming pool'
};
storeによって、actionとstateがreducerに渡され、reducerで処理が行われます。
◆store
ReducerとStateにアクセスできるオブジェクト
reduxを調べるとこんな図がよく出てきます。
ふむふむよくわかる。。って分かりませんよね。
最初はよく分かんなくていいと思います。(勝手。)
私も概念はまだよく理解していませんが、
reduxを利用するには、
ActionとかStoreを決まったように書いて(あるものをコピペして名前変えて)
って使ってます。
でも最初にredux使える環境を作る時には困りますかね。。。。
Reduxの例え話
例え話を考えてみました。
Reduxの例え話を考えてみた。
物(Redux state)
実際の物はお店(store)にあります。
・テキストA原本
・テキストB原本
指示看板(action type)と指示に使用する値
指示看板(テキストAに追記して)
+
値:2
とか
指示看板(テキストAを上書きして)
+
値:1
お店(store)
お店には、指示看板を処理する変更人(reducer)って人がいます。
dispatch
指示看板(action type)と指示に使用する値、つまりactionを
お店に送ります(dispatch)します。
お店にいる変更人(reducer)は
指示看板(action type)と指示に使用する値、つまりactionを元に
新しい値に変更します。
selector
後日、自分の子会社(子コンポーネント)がお店(store)に対して、テキストAの印刷したやつくれないかと言ってもらう(selector)
どうですか。
苦しいですかね。