1. とりあえず React だけで Todo - Redux よくわからんので Todo つくる

## はじめに

> このごろ作業している React アプリの状態管理がごちゃごちゃだぁ！
> どうやら Redux を使うといいらしい！
> よさそう！でもよくわからん！！
> これは一旦小規模なアプリをつくって理解を深めるべきだ！

...というわけで、Redux のあれこれを理解するため Todo アプリを作成します
こういう時に作るものは Todo アプリと相場が決まっているので

ただ、タイトルにある通り、現在 React で管理されている State を Redux に移行する流れを掴みたいので、一度 React のみで作成し、次回 Redux を組み込みます
今回 Redux は微塵も出てきません


## 環境構築
とりあえず React で Todo アプリを作成するために必要な環境を整えます

(( Rollup を使っているのは、今回はスタイリングをしないしと webpack のインストールが遅いのをサボっただけで、動けばええやろの精神なのであんまり参考にしないで & 詳しい人間違ってたら教えて下さい ))


### 依存パッケージのインストール
```shell
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
```

<details><summary>依存パッケージの一覧</summary><div>
> react
> react-dom
>
> 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
</div></details>


### コンパイル用のファイル

```js:rollup.config.js
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"),
  ],
};
```

```json-doc:.babelrc
{
  "presets": [
    "@babel/preset-react", // React のコンパイル
    "@babel/preset-typescript" // TypeScript のコンパイル
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread" // スプレッド構文
  ]
}
```

```json-doc:tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "strict": true,
    "moduleResolution": "node", // 相対パスでないモジュールは node_modules から
    "esModuleInterop": true, // commonjs モジュールを default import 出来るらしい？
    "jsx": "preserve",
    "sourceMap": true
  }
}
```


### Hello Wrold

とりあえず表示するファイル

```html:docs/index.html
<!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>
```

```tsx:src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";

ReactDOM.render(<App />, document.getElementById("app"));
```

```tsx:src/app/index.tsx
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 の管理と、受け渡しします

```tsx:src/app/index.tsx
...
const App: React.FC = () => {
  return (
    <React.Fragment>
      <AddTodo />
      <ToggleFilter />
      <TodoList />
    </React.Fragment>
  );
};
...
```


#### AddTodo

入力されたテキストから Todo を作成し App に渡します

```tsx:src/app/add-todo/index.tsx
...
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

フィルタ変更用のボタンを表示します

```tsx:src/app/toggle-filter/index.tsx
...
const ToggleFilter: React.FC = () => {
  return (
    <div>
      {// ALL,COMPLETED,ACTIVE でみっつ}
      <FilterLink />
      <FilterLink />
      <FilterLink />
    </div>
  );
};
...
```


#### FilterLink

実際のフィルタ変更ボタンです
フィルタ変更関数を受け取り、クリック時に変更します

```tsx:src/app/toggle-filter/filter-link/index.tsx
...
// disabled: 選択状態の場合ボタンを無効化
const FilterLink: React.FC = () => {
  const handler = {
    onClick: () => {} // App にフィルタの変更を伝達
  };

  return (
    <button onClick={handler.onClick}>
      {// ラベル}
    </button>
  );
};
...
```


#### TodoList

TodoList を受け取り、Todo を表示します

```tsx:src/app/todo-list/index.tsx
...
const TodoList: React.FC = ({todoList}) => {
  return (
    <ul>
      {todoList.map((todo) => (
        <Todo />
      ))}
    </ul>
  );
};
...
```


#### Todo

Todo を受け取り、表示します

```tsx:src/app/todo-list/todo/index.tsx
...
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 は趣味です オブジェクトの操作も新しいオブジェクトの作成も簡単

```ts:src/app/types.ts
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、変更のための関数を作成し、子コンポーネントに受け渡します

```tsx:src/app/index.tsx
...
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 を実行するコードを追加します

```tsx:src/app/add-todo/index.tsx
...
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 を受け渡します

```tsx:src/app/toggle-filter/index.tsx
...
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 に変更するコードを追加します

```tsx:src/app/toggle-filter/filter-link/index.tsx
...
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 を受け渡します

```tsx:src/app/todo-list/index.tsx
...
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 の削除処理を追加します

```tsx:src/app/todo-list/todo/index.tsx
...
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-todo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525849/8be39660-25f2-3509-8e34-374ce61927c0.gif)

これで React で State 管理された Todo アプリが出来ました
今回作成したコードは [こちら (canoypa/react-redux-test-todo-app)](https://github.com/canoypa/react-redux-test-todo-app/tree/only-react) にあります

というわけで、次回は当初の目的である State 管理の React から Redux への移行を行います

Next: [State 管理を Redux に移行する](https://qiita.com/canoypa/items/d2083ebda63e4970e398)
今回作ったアプリの State 管理を Redux に移行します


## 参考
[Example: Todo List | Redux](https://redux.js.org/basics/example)