はじめに
このごろ作業している React アプリの状態管理がごちゃごちゃだぁ!
どうやら Redux を使うといいらしい!
よさそう!でもよくわからん!!
これは一旦小規模なアプリをつくって理解を深めるべきだ!
...というわけで、Redux のあれこれを理解するため Todo アプリを作成します
こういう時に作るものは Todo アプリと相場が決まっているので
ただ、タイトルにある通り、現在 React で管理されている State を Redux に移行する流れを掴みたいので、一度 React のみで作成し、次回 Redux を組み込みます
今回 Redux は微塵も出てきません
環境構築
とりあえず React で Todo アプリを作成するために必要な環境を整えます
(( Rollup を使っているのは、今回はスタイリングをしないしと webpack のインストールが遅いのをサボっただけで、動けばええやろの精神なのであんまり参考にしないで & 詳しい人間違ってたら教えて下さい ))
依存パッケージのインストール
yarn add react react-dom
yarn add -D typescript @types/react @types/react-dom rollup rollup-plugin-serve rollup-plugin-terser @rollup/plugin-babel @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-replace @babel/core @babel/preset-react @babel/preset-typescript @babel/plugin-proposal-object-rest-spread
依存パッケージの一覧
コンパイル用のファイル
import commonjs from "@rollup/plugin-commonjs"; // commonjs の参照に使ってそう
import babel from "@rollup/plugin-babel"; // ばべる
import resolve from "@rollup/plugin-node-resolve"; // node_modules/ の参照に使ってそう
import serve from "rollup-plugin-serve"; // dev server
import { terser } from "rollup-plugin-terser"; // 圧縮 気分
import replace from "@rollup/plugin-replace"; // react devtool のなんかで必要らしい 参考: https://github.com/rollup/rollup/issues/487
export default {
input: "src/index.tsx", // エントリポイント
output: {
dir: "docs", // 出力フォルダ
format: "es", // 今回は意味無し モジュールの出力形式
sourcemap: true, // ソースマップ
},
plugins: [
terser(),
replace({
"process.env.NODE_ENV": JSON.stringify("development"),
}),
babel({ extensions: [".js", ".ts", ".tsx"] }),
resolve(),
commonjs({ extensions: [".js", ".ts", ".tsx"] }),
serve("docs"),
],
};
{
"presets": [
"@babel/preset-react", // React のコンパイル
"@babel/preset-typescript" // TypeScript のコンパイル
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread" // スプレッド構文
]
}
{
"compilerOptions": {
"target": "ESNext",
"strict": true,
"moduleResolution": "node", // 相対パスでないモジュールは node_modules から
"esModuleInterop": true, // commonjs モジュールを default import 出来るらしい?
"jsx": "preserve",
"sourceMap": true
}
}
Hello Wrold
とりあえず表示するファイル
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>React + Redux Todo App</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script type="module" defer src="index.js"></script>
</head>
<body>
<div id="app"></div>
</body>
</html>
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
ReactDOM.render(<App />, document.getElementById("app"));
import React from "react";
const App: React.FC = () => <div>Hello World</div>;
export default App;
ビルドして画面に Hello World
が表示されれば準備完了です
Todo アプリつくる
開発環境が整ったのでここからは Todo アプリを作っていきます
必要なコンポーネント
App
├ AddTodo - Todo 追加する
├ ToggleFilter - フィルタ変更する (全て/完了/未完了)
│ └ FilterLink - 実際のボタン
└ TodoList - Todo 表示する
└ Todo - Todo
雛形を作成
ちゃんと設計してから作らなかったりが悪いんですが、
変更の多い初期の段階から複数コンポーネントに跨る State を作成すると、どのコンポーネントが何をどう使ってるかよくわからなくなり、気づかないうちに他コンポーネントの破壊的変更を招く恐れがある(あった)ので、ひとまず他コンポーネントに影響しない規模で作成し State の形を確立することで、コンポーネント同士の整合性を確保します 🥺
App
必要なコンポーネントを読み込みます
最終的にこの子が Todo, Filter の管理と、受け渡しします
...
const App: React.FC = () => {
return (
<React.Fragment>
<AddTodo />
<ToggleFilter />
<TodoList />
</React.Fragment>
);
};
...
AddTodo
入力されたテキストから Todo を作成し App に渡します
...
const AddTodo: React.FC = () => {
const [text, setText] = React.useState<string>(""); // テキスト管理
const handler = {
addTodo: (event: React.FormEvent) => {
// App に Todo 追加を伝達
event.preventDefault();
},
setText: (event: React.ChangeEvent<HTMLInputElement>) => setText(event.target.value) // テキスト変更
};
return (
<form onSubmit={handler.addTodo}>
<input type="text" value={text} onChange={handler.setText} />
<button type="submit">Add</button>
</form>
);
};
...
ToggleFilter
フィルタ変更用のボタンを表示します
...
const ToggleFilter: React.FC = () => {
return (
<div>
{// ALL,COMPLETED,ACTIVE でみっつ}
<FilterLink />
<FilterLink />
<FilterLink />
</div>
);
};
...
FilterLink
実際のフィルタ変更ボタンです
フィルタ変更関数を受け取り、クリック時に変更します
...
// disabled: 選択状態の場合ボタンを無効化
const FilterLink: React.FC = () => {
const handler = {
onClick: () => {} // App にフィルタの変更を伝達
};
return (
<button onClick={handler.onClick}>
{// ラベル}
</button>
);
};
...
TodoList
TodoList を受け取り、Todo を表示します
...
const TodoList: React.FC = ({todoList}) => {
return (
<ul>
{todoList.map((todo) => (
<Todo />
))}
</ul>
);
};
...
Todo
Todo を受け取り、表示します
...
const AddTodo: React.FC = () => {
const handler = {
toggleCompleted: () => {}, // 完了/未完了
deleteTodo: () => {}, // 削除
};
return (
<li>
<span>{// テキスト}</span>
<button onClick={handler.toggleCompleted}>
{// 完了してる ? "Incomplete" : "Complete"}
</button>
<button onClick={handler.deleteTodo}>Delete</button>
</li>
);
};
...
State 実装
というわけで、雛形を作っておいたおかげか、単に単純だからかわかりませんが、悩むことなく State の構造を確立しました
Map は趣味です オブジェクトの操作も新しいオブジェクトの作成も簡単
export type TodoType = { id: number; text: string; complete: boolean };
export type TodoListType = Map<number, TodoType>;
export type FilterType = "ALL" | "COMPLETED" | "ACTIVE";
これを元に、State を実装していきます
App
Todo, Filter の State、変更のための関数を作成し、子コンポーネントに受け渡します
...
const App: React.FC = () => {
// Todo
const [todoList, setTodo] = React.useState<TodoListType>(new Map());
// Filter
const [filter, setFilter] = React.useState<FilterType>("ALL");
const handler = {
// Todo を追加する
addTodo: (todo: TodoType) => setTodo(new Map(todoList.set(todo.id, todo))),
// Todo を削除する
deleteTodo: (id: number) => {
todoList.delete(id);
setTodo(new Map(todoList));
},
// 完了/未完了 の切り替え
toggleCompleted: (id: number, complete: boolean) => {
const todo = todoList.get(id);
if (todo) {
const newTodo = { ...todo, complete};
setTodo(new Map(todoList.set(todo.id, newTodo)));
}
}
};
return (
<React.Fragment>
{// Todo 追加のための関数を渡す}
<AddTodo addTodo={handler.addTodo} />
{// Filter 変更のための関数と、ボタンの disabled 用に現在のフィルタを渡す}
<ToggleFilter setFilter={setFilter} activeFilter={filter} />
{// Todo リスト、更新用関数、削除用関数、フィルタ用に現在のフィルタを渡す}
<TodoList
todoList={todoList}
updateTodo={handler.updateTodo}
deleteTodo={handler.deleteTodo}
filter={filter}
/>
</React.Fragment>
);
};
...
AddTodo
Submit 時に Todo を作成し addTodo を実行するコードを追加します
...
type Props = {
addTodo: (todo: TodoType) => void;
};
const AddTodo: React.FC<Props> = ({addTodo}) => {
const [text, setText] = React.useState<string>("");
const handler = {
addTodo: (event: React.FormEvent) => {
// Todo を作成し、受け取った addTodo を実行
// id はクソ適当
const todo = { id: Math.random(), text, complete: false };
addTodo(todo);
event.preventDefault();
},
...
ToggleFilter
FilterLink に setFilter, filter, disabled, label を受け渡します
...
type Props = {
setFilter: (filter: FilterType) => void;
activeFilter: FilterType;
};
const ToggleFilter: React.FC<Props> = ({setFilter, activeFilter}) => {
return (
<div>
{// フィルタ変更用の関数、担当のフィルタ、現在アクティブか、を渡す}
<FilterLink
setFilter={setFilter}
filter={"ALL"}
disabled={activeFilter === "ALL"}
label="ALL"
/>
<FilterLink
setFilter={setFilter}
filter={"COMPLETED"}
disabled={activeFilter === "COMPLETED"}
label="COMPLETED"
/>
<FilterLink
setFilter={setFilter}
filter={"ACTIVE"}
disabled={activeFilter === "ACTIVE"}
label="ACTIVE"
/>
</div>
);
};
...
FilterLink
クリック時に受け取った filter に変更するコードを追加します
...
type Props = {
setFilter: (filter: FilterType) => void;
filter: FilterType;
disabled: boolean;
label: string;
};
const FilterLink: React.FC<Props> = ({setFilter, filter, disabled, label}) => {
const handler = {
onClick: () => setFilter(filter),
};
return (
<button onClick={handler.onClick} disabled={disabled}>
{label}
</button>
);
};
...
TodoList
受け取った TodoList(Map) を Array に変換し、フィルタにかけ表示します
また、Todo に todo, toggleCompleted, deleteTodo を受け渡します
...
type Props = {
todoList: TodoListType;
filter: FilterType;
toggleCompleted: (id: number, isCompleted: boolean) => void;
deleteTodo: (id: number) => void;
};
const TodoList: React.FC<Props> = ({todoList, filter, toggleCompleted, deleteTodo}) => {
// フィルタしたりするので Array に変換
const todoListArr = [...todoList.values()];
// フィルタ
const filteredTodoList = todoListArr.filter((todo) => {
if (filter === "COMPLETED" && todo.complete) return true;
if (filter === "ACTIVE" && !todo.complete) return true;
if (filter === "ALL") return true;
});
return (
<ul>
{filteredTodoList.map((todo) => (
<Todo todo={todo} toggleCompleted={toggleCompleted} deleteTodo={deleteTodo} />
))}
</ul>
);
};
...
Todo
Todo を表示し、完了/未完了 の切り替え、Todo の削除処理を追加します
...
type Props = {
todo: TodoType;
toggleCompleted: (id: number, isCompleted: boolean) => void;
deleteTodo: (id: number) => void;
};
const AddTodo: React.FC<Props> = ({todo, toggleCompleted, deleteTodo}) => {
const handler = {
// 完了/未完了 の切り替え
toggleCompleted: () => {
toggleCompleted(todo.id, !todo.complete);
},
// 削除
deleteTodo: () => {
deleteTodo(todo.id);
},
};
return (
<li>
<span>{todo.text}</span>
<button onClick={handler.toggleCompleted}>
{todo.complete ? "Incomplete" : "Complete"}
</button>
<button onClick={handler.deleteTodo}>Delete</button>
</li>
);
};
...
完成
これで React で State 管理された Todo アプリが出来ました
今回作成したコードは こちら (canoypa/react-redux-test-todo-app) にあります
というわけで、次回は当初の目的である State 管理の React から Redux への移行を行います
Next: State 管理を Redux に移行する
今回作ったアプリの State 管理を Redux に移行します