1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-11

はじめに

このごろ作業している 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
依存パッケージの一覧
> 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

コンパイル用のファイル

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"),
  ],
};
.babelrc
{
  "presets": [
    "@babel/preset-react", // React のコンパイル
    "@babel/preset-typescript" // TypeScript のコンパイル
  ],
  "plugins": [
    "@babel/plugin-proposal-object-rest-spread" // スプレッド構文
  ]
}
tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "strict": true,
    "moduleResolution": "node", // 相対パスでないモジュールは node_modules から
    "esModuleInterop": true, // commonjs モジュールを default import 出来るらしい?
    "jsx": "preserve",
    "sourceMap": true
  }
}

Hello Wrold

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

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>
src/index.tsx
import React from "react";
import ReactDOM from "react-dom";
import App from "./app";

ReactDOM.render(<App />, document.getElementById("app"));
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 の管理と、受け渡しします

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

AddTodo

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

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

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

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

FilterLink

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

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 を表示します

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

Todo

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

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 は趣味です オブジェクトの操作も新しいオブジェクトの作成も簡単

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

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 を実行するコードを追加します

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 を受け渡します

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 に変更するコードを追加します

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 を受け渡します

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 の削除処理を追加します

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

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

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

Next: State 管理を Redux に移行する
今回作ったアプリの State 管理を Redux に移行します

参考

Example: Todo List | Redux

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?