Try   HackMD

Todo リストを作る

tags: React, TyapeScript

React + TypeScript で、Todo リストを作ります。

完成図

リポジトリを用意する

実装手順

  1. テンプレートリポジトリ をクローンする

    リポジトリ名は todo-app とします。

  2. VSCode で開き、ローカルホストを立ち上げる

Todo モデルの作成

実装手順

  1. Todo モデルの作成 行頭 + で始まるコードを追加してください。

    pages/index.tsx

    ​​​import Head from "next/head";
    ​​​import type { NextPage } from "next";
    
    ​​​+ type Todo = {
    ​​​+   id: number
    ​​​+   name: string
    ​​​+   isDone: boolean
    ​​​+ }
    
    ​​​const Home: NextPage = () => {
    
  2. モックデータ(ダミーデータ)の作成 コードを追加してください。 pages/index.tsx

    ​​​export type Todo = {
    ​​​ id: number
    ​​​ name: string
    ​​​ isDone: boolean
    ​​​}
    
    ​​​+ const mockTodo0: Todo = {
    ​​​+   id: 0,
    ​​​+   name: "髪を切りに行く",
    ​​​+   isDone: false,
    ​​​+ }
    ​​​+ const mockTodo1: Todo = {
    ​​​+   id: 1,
    ​​​+   name: "プレゼントを選ぶ",
    ​​​+   isDone: false,
    ​​​+ }
    ​​​+ const mockTodo2: Todo = {
    ​​​+   id: 2,
    ​​​+   name: "映画館デートする",
    ​​​+   isDone: false,
    ​​​+ }
    
    ​​​+ const mockTodoList = [mockTodo0, mockTodo1, mockTodo2]
    
    ​​​const Home: NextPage = () => {
    

解説

モデルとは?

モデルとは、「ユーザーの目当て」です。今回作成する Todo アプリのモデルは、Todo 単体です。なぜなら、アプリの使用中、ユーザーの関心は Todo に向かっているからです。「Todo を登録」「Todo を削除」「Todo を編集」「Todo を Done にする」など、これらのタスクを実行するとき、ユーザーの関心は Todo にあると言えます。

モデルの型定義

TypeScript では、モデルの型定義ができます。今回の実装の以下の箇所で、Todo モデルの型定義が行われています。

pages/index.tsx

type Todo = {
  id: number;
  name: string;
  isDone: boolean;
};

ここで Todo の型は、number 型のidプロパティ、string 型のnameプロパティ、boolean 型のisDoneプロパティをもったオブジェクトと定義されています。

プロパティとオブジェクト

オブジェクトとは、プロパティの集合です。プロパティとは名前(キー)と値(バリュー)が対になったものです。オブジェクトは、{}(カーリーブレイス)で作成します。

const sampleObj = {
  // キー: 値
  key: "value",
};

今回の実装だと、モックデータとして mock0、mock1、mock2 の 3 つのオブジェクトを作成しました。

pages/index.tsx

const mock0: Todo = {
  id: 0,
  name: "髪を切りに行く",
  isDone: false,
};
const mock1: Todo = {
  id: 1,
  name: "プレゼントを選ぶ",
  isDone: false,
};
const mock2: Todo = {
  id: 2,
  name: "映画館デートする",
  isDone: false,
};

型定義をする理由

なぜ、型を定義するのでしょうか?筆者は、型定義には 2 つのメリットがあると考えています。

  1. コードの可読性があがる
  2. 予期せぬバグが起こるの防ぐ

今回の実装から理解できる範囲で説明します。作成したモックデータを見てください。以下の部分は、「mock0 は Todo の型に従わなければならない」ということを意味しています。このようにmock0の型を明示したことによって、mock0は Todo のモックデータなのだということがひと目で分かるようになっています。(逆に、: Todoの記述がなかった場合と比較してみてください。)これが 1 つ目のメリット、コードの可読性が上がると考える理由です。

pages/index.tsx

const mock0: Todo = {
  // 省略
};

次に、作成したモックデータを以下のように壊してみます。するとエラー文が出てくるはずです。

pages/index.tsx

const mock0: Todo = {
  id: 0,
  name: "髪を切りに行く",
+  place: "バーバー井上",
  isDone: false,
};

エラー文

Type '{ id: number; name: string; place: string; isDone: false; }' is not assignable to type 'Todo'.

TypeScript は指定した型を破ると即座にエラーを出してくれます。型定義を利用することでコードの振る舞いを限定し、予期せぬバグを防ぐことができます。これが 2 つ目のメリット、予期せぬバグが起こるの防げると考える理由です。

配列

配列とは、値に順序をつけて格納できるオブジェクトです。 配列に格納したそれぞれの値のことを要素、それぞれの要素の位置のことをインデックス(index)と呼びます。 インデックスは先頭の要素から 0、1、2 のように 0 からはじまる連番となります。配列は、[](ブラケット)を使って作成することができます。今回の実装の mockTodoList が配列です。

pages/index.tsx

const mockTodoList = [mock0, mock1, mock2];

配列にマウスを当てると、その型を確認できます。mockTodoList の型を確認してみましょう。

const mockTodoList: Todo[];

これは、「mockTodoListの型 は Todo 型の要素の配列型」ということです。mockTodoList の型は、コードで指定したものではなく、TypeScript の型推論によって自動的に指定されたものです。

まとめ

  • モデルと型定義を理解する
  • Todo モデルの型定義
  • Todo モデルの型に従ったモックデータ(ダミーデータ)を作成

Todo リストを表示する

実装手順

  1. ステート todoList を作る コードを追加してください。

    pages/index.tsx

    ​​​export const Home: VFC = () => {
    ​​​+  const [todoList, setTodoList] = useState(mockTodoList)
    
  2. todoList の中身を表示するコードを追加してください。

    pages/index.tsx

    ​​​<main className="flex flex-col justify-center items-center py-4 px-8 m-0 min-h-screen">
    ​​​+ <table className="text-sm text-left text-gray-500 table-auto">
    ​​​+   <thead className="text-xs text-gray-700 uppercase bg-gray-50 ">
    ​​​+     <tr>
    ​​​+       <th className="py-4 px-6">ステータス</th>
    ​​​+       <th className="py-4 px-6">名前</th>
    ​​​+       <th></th>
    ​​​+       <th className="py-4 px-6"></th>
    ​​​+     </tr>
    ​​​+   </thead>
    ​​​+   <tbody>
    ​​​+     {todoList.map((todo) => (
    ​​​+       <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​+        <td></td>
    ​​​+         <td className="py-4 px-6">
    ​​​+           <p
    ​​​+             className="w-40"
    ​​​+             style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​+           >
    ​​​+             {todo.name}
    ​​​+           </p>
    ​​​+         </td>
    ​​​+         <td></td>
    ​​​+         <td></td>
    ​​​+       </tr>
    ​​​+     ))}
    ​​​+   </tbody>
    ​​​+ </table>
    ​​​</main>
    

解説

ステート の型

useState で生成されたステートにも型があります。ステートの型は、ステートにマウスをホバーすることで確認できます。todoList の型を確認しましょう。

const todoList: Todo[];

ステートの初期値 mockTodoList から型推論によって、 Todo[] が指定されています。

オブジェクト内のプロパティの値を取得する

オブジェクト内のプロパティの値を取得する方法を学びましょう。例えば、idInMock0mock0オブジェクトの idの値 0 を代入する場合、以下のようになります。

const mock0 = {
  id: 0,
  name: "髪を切りに行く",
  isDone: false,
};

const idInMock0 = mock0.id;

map 関数

map 関数とは、配列の各要素に対して指定された関数を実行し、その結果から新しい配列を作成する関数です。今回の実装を確認します。

pages/index.tsx

{
  todoList.map((todo) => (
    <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
      <td></td>
      <td className="py-4 px-6">
        <p
          className="w-40"
          style={{ textDecoration: todo.isDone ? "line-through" : "" }}
        >
          {todo.name}
        </p>
      </td>
      <td></td>
      <td></td>
    </tr>
  ));
}

todoList の各要素に対して、map関数の引数に渡された関数 (todo) => <li key={todo.id}>{todo.name}</li> を実行し、新しい配列を返しています。 todo とは、todoList の各要素のことだと考えてください。todoList が初期値の場合、上記コードの処理結果は以下のようになります。

{[
  <tr key={0}>
      <td></td>
      <td className="py-4 px-6">
        <p
          className="w-40"
          style={{ textDecoration: todo.isDone ? "line-through" : "" }}
        >
          髪を切りに行く
        </p>
      </td>
      <td></td>
      <td></td>
  </tr>,
  <tr key={1}>
      <td></td>
      <td className="py-4 px-6">
        <p
          className="w-40"
          style={{ textDecoration: todo.isDone ? "line-through" : "" }}
        >
          プレゼントを選ぶ
        </p>
      </td>
      <td></td>
      <td></td>
  </tr>,
  <tr key={2}>
      <td></td>
      <td className="py-4 px-6">
        <p
          className="w-40"
          style={{ textDecoration: todo.isDone ? "line-through" : "" }}
        >
          映画館デートする
        </p>
      </td>
      <td></td>
      <td></td>
  </tr>
]}

複数行の JSX だけを返す関数の書き方

今回の実装で map 関数に渡されていた関数は、複数行の JSX だけを返す関数です。複数行の JSX だけを返す関数は 2 つの書き方があります。今回の実装では、省略形が採用されています。 => に続く括弧が異なっていることに注意してください。

const function0 = (todo) => {
  return (
    <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
      // 省略
    </tr>
  );
};

// 省略形
const function1 = (todo) => (
  <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    // 省略
  </tr>
);

まとめ

  • ステート(todoList)を作成
  • todoList の中身を、map 関数で表示

Todo アプリの全体像を理解する

Todo アプリの仕様

今回の Todo アプリには以下の 6 つの機能を実装します。「Todo を表示する」は、前のセクションで実装済みです。

  • Todo を表示
  • 新しい Todo を作成
  • Todo の削除
  • 既存の Todo の編集
  • Todo のステータスを Done(完了)にする
  • Done になった Todo を削除する

ステート todoList の更新

上記 6 つの機能を、todoList を使って実装します。ここでは、どのような実装になるかの概要を見ていきます。

Todo を表示

配列 todoList 内の要素を表示する。

新しい Todo を作成

配列 todoList に、要素を追加する。追加される要素は、Todo モデルの型に従っている。

Todo の削除

配列 todoList から、任意の id を持った要素を削除する。

既存の Todo の編集

配列 todoList 内の任意の id を持った要素の name を変更する。

Todo のステータスを Done(完了)にする

配列 todoList 内の任意の id を持った要素の isDonefalse から true に変更する。

Done になった Todo を削除する

配列 todoList から、 isDone が true の要素を削除する。

Todo 登録フォームを作る

このセクションでは、新しい Todo を登録するためのフォームを実装します。

実装手順

  1. 入力フォームの追加

    pages/index.tsx

    ​​​<main className="flex flex-col justify-center items-center py-4 px-8 m-0 min-h-screen">
    ​​​+  <input type="text" className="border" />
    ​​​  <table>
    ​​​    {todoList.map((todo) => (
    ​​​     // 省略
    ​​​    ))}
    ​​​  </table>
    ​​​</main>
    
  2. フォームにされた内容をステート text で管理する

    pages/index.tsx

    ​​​const Home: NextPage = () => {
    ​​​  const [todoList, setTodoList] = useState(mockTodoList)
    ​​​+  const [text, setText] = useState("")
    
    ​​​+  const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    ​​​+    setText(e.target.value)
    ​​​+  }
    

    先程、作成したフォームを以下のように修正してください。

    pages/index.tsx

    ​​​-  <input type="text" className="border"/>
    ​​​+   <input
    ​​​+     type="text"
    ​​​+     value={text}
    ​​​+     onChange={handleChangeInput}
    ​​​+     className="border"
    ​​​+   />
    

解説

実装した input

こちらの記事を参考に解説します。

inputvalue

input は、value を持っています。フォームに入力された内容は value に保存されます。

イベント

イベントとは、アプリ内のユーザーの動作、出来事を指します。必要であれば、イベントに対して、何らかの反応を返す事ができます。その際用いるのがイベントプロパティです。イベントプロパティはいろいろあります。 例えば onClick は「ユーザーがボタンをクリックしたとき」に何らかの処理を実行します。

<button onClick={<クリックされたときに行う処理>}>ボタン</button>

また、イベントプロパティには型があります。TypeScript では、以下のように表現されます。使い方は後述します。

// イベントプロパティ: イベントプロパティの型
type Props = {
  onClick: (event: React.MouseEvent<HTMLInputElement>) => void;
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
  onkeypress: (event: React.KeyboardEvent<HTMLInputElement>) => void;
};

onChage

onChage はイベントプロパティの一つです。今回の実装では、「ユーザーがフォームに入力するとき」、つまりinputvalue に変更が起こったときに、onChange に渡された関数が実行されます。このとき実行される関数の引数には、イベントオブジェクトが渡されます。イベントオブジェクトから、イベントの様々な情報を得ることができます。

具体例を見ます。下のコードをコンポーネントの return 以下に追加してください。ローカルホストで開発者ツールの Console を開き、作成したフォームに入力します。Console に入力中の内容が表示されるはずです。

pages/index.tsx

<input
  className="border-2 border-red-500"
  onChange={(e) => console.log(e.target.value)}
/>

一連の流れを解説します。

  1. ユーザーがフォームに入力
  2. inputvalueが変化する
  3. onChange に渡された関数が呼び出される 今回の例だと、(e) => console.log(e.target.value)が呼び出される。

今回の実装だと、onChange から渡されるイベントオブジェクトは eと呼ばれています。 e.target.value で フォームに入力された内容、つまりinputvalueを取得することができます。

フォームの入力内容をステートとして管理する

Home コンポーネント内のreturnより上の箇所で、以下のコードを追加してください。

pages/index.tsx

const Home: NextPage = () => {
  const [todoList, setTodoList] = useState(mockTodoList)
  const [text, setText] = useState("")
+  const [demo, setDemo] = useState("")

+ console.log(demo)

先程のコードを以下のように変更します。

pages/index.tsx

<input
+  value={demo}
  className="border-2 border-red-500"
-  onChange={(e) => console.log(e.target.value)}
+  onChange={(e) => setDemo(e.target.value)}
/>

開発者ツールの Console を確認してみてください。 フォームの入力内容にあわせて demo が変更されていますね。これで、フォームの入力内容をステートとして管理することができました。一連の流れを説明します。

  1. ユーザーがフォームに入力
  2. inputvalueが変化する
  3. onChange に渡された関数が呼び出される
  4. ステート demo が更新される
実装箇所の解説

今回の実装を観察します。先程までの解説とほとんど同じですが、onChange に直接関数を書いて渡すのではなく、handleChangeInputという関数名を渡しています。onChange に渡される引数の型が e: React.ChangeEvent<HTMLInputElement> と指定されているところも確認してください。

pages/index.tsx

const [text, setText] = useState("");
const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
  setText(e.target.value);
};

// 再掲: onChange の型
type Props = { onChange: (event: React.ChangeEvent<HTMLInputElement>) => void };

pages/index.tsx

<input
  type="text"
  value={text}
  onChange={handleChangeInput}
  className="border"
/>

最後に、デモで使用したコードを次に進んでください。

pages/index.tsx

const Home: NextPage = () => {
  const [todoList, setTodoList] = useState(mockTodoList)
  const [text, setText] = useState("")
-  const [demo, setDemo] = useState("")

- console.log(demo)

pages/index.tsx

- <input
-   value={demo}
-  className="border-2 border-red-500"
-   onChange={(e) => console.log(e.target.value)}
-   onChange={(e) => setDemo(e.target.value)}
- />

Todo を登録できるようにする

このセクションでは、フォームの入力内容をもとに新しい Todo を作成できるようにします。

実装手順

  1. 登録ボタンの追加

    pages/index.tsx

    ​​​<main>
    ​​​+ <div className="flex flex-col gap-2 py-10">
    ​​​   <input
    ​​​     type="text"
    ​​​     value={text}
    ​​​     onChange={handleChangeInput}
    ​​​     className="border"
    ​​​   />
    ​​​+ <button
    ​​​+   className="py-2 px-4 font-bold text-white bg-blue-500 hover:bg-blue-400 rounded border-b-4 border-blue-700 hover:border-blue-500"
    ​​​+    disabled={text === ""}
    ​​​+   >
    ​​​+     Todo登録
    ​​​+   </button>
    ​​​+ </div>
    
  2. 以下のコードを追加してください

    pages/index.tsx

    ​​​+ let nextId = 3
    
    ​​​const Home: NextPage = () => {
    
  3. 入力内容を Todo に登録する

    pages/index.tsx

    ​​​const Home: NextPage = () => {
    ​​​  const [todoList, setTodoList] = useState<Todo[]>(mockTodoList)
    ​​​  const [text, setText] = useState("")
    
    ​​​  const handleChangeInput: ChangeEventHandler<HTMLInputElement> = (e) => {
    ​​​    setText(e.target.value)
    ​​​  }
    
    ​​​+ const register = () => {
    ​​​+   const newTodo: Todo = {
    ​​​+     id: nextId,
    ​​​+     name: text,
    ​​​+     isDone: false,
    ​​​+   }
    ​​​+   setTodoList([...todoList, newTodo])
    ​​​+
    ​​​+   setText("")
    ​​​+   nextId++
    ​​​+ }
    

    先程作成したボタンを以下のように修正してください。

    pages/index.tsx

    ​​​<button
    ​​​+  onClick={register}
    ​​​  className="py-2 px-4 font-bold text-white bg-blue-500 hover:bg-blue-400 rounded border-b-4 border-blue-700 hover:border-blue-500"
    ​​​  disabled={text === ""}
    ​​​>
    ​​​  Todo登録
    ​​​</button>
    

解説

const

constとは、再代入できない変数の宣言とその変数が参照する値(初期値)を定義できます。

const 変数名 = 初期値;

再代入しようとするとエラーがでます。

const name = "Kosuke";
name = "Takashi"; // => エラー:Cannot assign to 'name' because it is a constant.

let

letとは、値の再代入が可能な変数を宣言できます。使い方は const とほぼ同じです。

let name = "Kosuke";
name = "Takashi";

今回の実装では、唯一 nextIDletで宣言されています。

let nextId = 3;

インクリメント演算子++

インクリメント演算子++は、オペランド(被演算子)をインクリメント (1 を加算) して値を返します。

let x = 3;
y = x++;

// y = 3
// x = 4

今回の実装を確認してみましょう。変数nextIDがインクリメントされています。

pages/index.tsx

const register = () => {
  // 省略

  setText("");
  nextId++;
};

nextId++ は、以下のコードの省略形です。 nextIDnextID をインクリメントした値が再代入されています。

nextId = nextId++;

配列 TodoList に追加する要素を作成する

register 内の処理を確認します。まず、todoList に追加するnewTodo を作成します。ポイントは 2 つです。

  • newTodoは Todo モデルの型に従っている
  • name の値は text

pages/index.tsx

const register = () => {
  const newTodo: Todo = {
    id: nextId,
    name: text,
    isDone: false,
  };
  // 省略
};

spread syntax ...

spread syntax ...を使うことで、要素を展開することができます。

const arr = [1, 2, 3];
const arr2 = [...arr, 4];

console.log(arr2); // => expected output is&nbsp;[1, 2, 3, 4]

今回の実装では、 register の中で使用されています。

pages/index.tsx

const register = () => {
  const newTodo: Todo = {
    id: nextId,
    name: text,
    isDone: false,
  };
  setTodoList([...todoList, newTodo]);

  setText("");
  nextId++;
};

todoList が初期値の場合、spread syntax では以下のように展開されます。

setTodoList([mock0, mock1, mock2, newTodo]);

まとめ

register の処理を確認します。

  1. フォームに新たに追加したい Todo が入力される
  2. 「Todo に登録」ボタンをクリックされる
  3. フォームに入力内容を nameに持った newTodoを作成
  4. todoList内の要素に newTodoを追加した配列を、新たに todoList にする
  5. フォームがリセット
  6. nextId がインクリメント

補足

idとは、各 Todo の識別子の役割を果たしています。Todo が作られるたびに、nextId をインクリメントさせることで、各 Todo が固有の id を持つようにしています。今回の実装では、すでにモックデータで 0、1、2 の id が使われていたので、 let nextId = 3 としました。最初に作られる Todo の id は 3、それ以降、Todo の id は 1 ずつ大きくなります。

Todo 削除ボタンを作る

このセクションでは、Todo を削除する機能を実装します。

実装手順

  1. 削除ボタンの作成

    pages/index.tsx

    ​​​{todoList.map((todo) => (
    ​​​   <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​     <td></td>
    ​​​     <td className="py-4 px-6">
    ​​​       <p
    ​​​         className="w-40"
    ​​​         style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​       >
    ​​​         {todo.name}
    ​​​       </p>
    ​​​     </td>
    ​​​     <td></td>
    ​​​     <td className="py-4 px-6">
    ​​​+     <button className="py-2 px-4 font-bold text-white bg-red-500 rounded">
    ​​​+       削除
    ​​​+     </button>
    ​​​+   </td>
    ​​​   </tr>
    ​​​ ))}
    
  2. remove 関数の用意

    pages/index.tsx

    ​​​const register = () => {
    ​​​  // 省略
    ​​​  }
    
    ​​​+ const remove = (id: number) => {
    ​​​+     const newState = todoList.filter((todo) => todo.id !== id)
    ​​​+     setTodoList(newState)
    ​​​+   }
    

    pages/index.tsx

    ​​​- <button className="py-2 px-4 font-bold text-white bg-red-500 rounded">
    ​​​+ <button
    ​​​+    onClick={() => remove(todo.id)}
    ​​​+    className="py-2 px-4 font-bold text-white bg-red-500 rounded"
    ​​​+ >
    ​​​   削除
    ​​​ </button>
    

解説

map 関数のおさらい

現在のコードで、map 関数のおさらいをしましょう。

pages/index.tsx

{
  todoList.map((todo) => (
    <button
      onClick={() => remove(todo.id)}
      className="py-2 px-4 font-bold text-white bg-red-500 rounded"
    >
      {todo.name}
    </button>
  ));
}

todoList が初期状態の場合、上記コードを map を使わないで書くと、以下のようになります。各 Todo が削除ボタンを持ちます。 onClick 内の remove は Todo の id を引数として受け取っています。

<button
  onClick={() => remove(0)}
  className="py-2 px-4 font-bold text-white bg-red-500 rounded"
>
  髪を切りに行く
</button>
<button
  onClick={() => remove(1)}
  className="py-2 px-4 font-bold text-white bg-red-500 rounded"
>
  プレゼントを選ぶ
</button>
<button
  onClick={() => remove(2)}
  className="py-2 px-4 font-bold text-white bg-red-500 rounded"
>
  映画館デートする
</button>

filter 関数

filter 関数とは、与えられた関数によって実装されたテストに合格したすべての配列からなる新しい配列を生成します。

const words = [
  "spray",
  "limit",
  "elite",
  "exuberant",
  "destruction",
  "present",
];

const result = words.filter((word) => word.length > 6);

console.log(result);
// expected output: Array ["exuberant", "destruction", "present"]

今回実装した remove を確認します。remove は、Todo の id を引数として受け取ります。 newState は、 todoList から、 remove の引数と一致する id を持った Todo が取り除かれた配列です。

pages/index.tsx

const remove = (id: number) => {
  const newState = todoList.filter((todo) => todo.id !== id);
  setTodoList(newState);
};

まとめ

  1. 削除ボタンが押され、その onClickに渡されている remove が実行される
  2. todoList から、削除したい Todo が削除される

Todo 完了ボタンを作る

このセクションでは、Todo を完了にする機能を実装します。

実装手順

  1. 完了チェックボックスの用意

    pages/index.tsx

    ​​​{todoList.map((todo) => (
    ​​​   <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​-    <td></td>
    ​​​+    <td className="py-4 px-6">
    ​​​+      <input
    ​​​+        type="checkbox"
    ​​​+        checked={todo.isDone}
    ​​​+      />
    ​​​+    </td>
    ​​​     <td className="py-4 px-6">
    ​​​       <p
    ​​​         className="w-40"
    ​​​         style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​       >
    ​​​         {todo.name}
    ​​​       </p>
    ​​​     </td>
    
  2. toggle 関数の作成

    pages/index.tsx

    ​​​const remove = (id: number) => {
    ​​​  // 省略
    ​​​};
    
    ​​​+ const toggle = (id: number) => {
    ​​​+   const newState = todoList.map((todo) => {
    ​​​+     if (todo.id !== id) return todo;
    ​​​+     return { ...todo, isDone: !todo.isDone };
    ​​​+   });
    ​​​+
    ​​​+   setTodoList(newState);
    ​​​+ };
    

    pages/index.tsx

    ​​​  <input
    ​​​    type="checkbox"
    ​​​    checked={todo.isDone}
    ​​​+    onChange={() => toggle(todo.id)}
    ​​​  />
    

解説

input type="checkbox"

<input type=“checkbox”>でチェックボックスが作れます。checked 属性を使うことで、チェックボックスの挙動をコントロールできます。 今回の実装を確認します。 isDone は Boolean 型でした。isDonetrue ならチェックが入り、false なら空欄になります。

pages/index.tsx

<input type="checkbox" checked={todo.isDone} onChange={() => toggle(todo.id)} />

mock0 の isDonefalse から true に変えて、ローカルホストを確認してみましょう。

pages/index.tsx

const mock0: Todo = {
  id: 0,
  name: "髪を切りに行く",
-  isDone: false,
+  isDone: true,
};

論理否定 (!)

論理否定 (!) 演算子 (論理反転、否定) は、真値を取ると偽値になり、偽値になると真値になります。 今回の実装を確認します。todo.isDone が true のとき isDone は false になり、todo.isDone が false のとき isDone は true になります。

isDone: !todo.isDone;

厳密不等価 (!==)

厳密不等価演算子 (!==) は、2 つのオペランド(被演算子)が等しくないことを検査し、truefalse で結果を返します。

console.log(1 !== 1);
// expected output: false
console.log("1" !== 1);
// expected output: true

if 文

if 文を使うことで、プログラム内に条件分岐を書けます。if 文は次のような構文が基本形となります。 条件式の評価結果が true であるならば、実行する文が実行されます。

if (条件式) {
  実行する文;
}

今回の実装を確認します。togggle の引数と todo.id が異なる場合は、todo がそのまま返されます。逆に、togggle の引数と todo.id が同じだった場合は、if 文に続く処理 return { ...todo, isDone: !todo.isDone } に移動します。

pages/index.tsx

const toggle = (id: number) => {
  const newState = todoList.map((todo) => {
    if (todo.id !== id) {
      return todo;
    }
    return { ...todo, isDone: !todo.isDone };
  });

  setTodoList(newState);
};

上記で書かれたコードは、以下のようにリファクタリングできます。

pages/index.tsx

const toggle = (id: number) => {
  const newState = todoList.map((todo) => {
    if (todo.id !== id) return todo;

    return { ...todo, isDone: !todo.isDone };
  });

  setTodoList(newState);
};

まとめ

  1. 完了した Todo にチェックが入れられる
  2. その Todo の isDonetrue になる

完了した Todo を一掃するボタンを作る

このセクションでは、完了した Todo を一掃する機能を実装します。

実装手順

  1. クリアボタンの作成

    pages/index.tsx

    ​​​<thead className="text-xs text-gray-700 uppercase bg-gray-50 ">
    ​​​ <tr>
    ​​​   <th className="py-4 px-6">ステータス</th>
    ​​​   <th className="py-4 px-6">名前</th>
    ​​​   <th></th>
    ​​​+   <th className="py-4 px-6">
    ​​​+     <button
    ​​​+       className="py-2 px-4 font-bold text-white bg-gray-700 hover:bg-gray-900 rounded"
    ​​​+     >
    ​​​+       一掃
    ​​​+     </button>
    ​​​   </th>
    ​​​ </tr>
    ​​​</thead>
    
  2. clear 関数の作成

    pages/index.tsx

    ​​​const toggle = () => {
    ​​​  // 省略
    ​​​};
    
    ​​​+ const clear = () => {
    ​​​+   const newState = todoList.filter((todo) => !todo.isDone);
    ​​​+   setTodoList(newState);
    ​​​+ };
    

    pages/index.tsx

    ​​​<button
    ​​​+   onClick={clear}
    ​​​   className="py-2 px-4 font-bold text-white bg-gray-700 hover:bg-gray-900 rounded"
    ​​​ >
    ​​​   一掃
    ​​​ </button>
    

Todo コンポーネントの作成

このセクションでは、Todo コンポーネントを作成します。

実装手順

  1. Todo コンポーネントを作成する まず、Home コンポーネントの上に、Todo コンポーネントを作ります。

    pages/index.tsx

    ​​​+ const Todo = () => {
    ​​​+   return (
    ​​​+   );
    ​​​+ };
    
    ​​​const Home: NextPage = () => {
    

    次に、Home コンポーネントの以下の箇所(行頭が - のコード)を切り取ります。

    pages/index.tsx

    ​​​<tbody>
    ​​​  {todoList.map((todo) => (
    ​​​-   <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​-           <td className="py-4 px-6">
    ​​​-             <input
    ​​​-               type="checkbox"
    ​​​-               checked={todo.isDone}
    ​​​-               onChange={() => toggle(todo.id)}
    ​​​-             />
    ​​​-           </td>
    ​​​-           <td className="py-4 px-6">
    ​​​-             <p
    ​​​-               className="w-40"
    ​​​-               style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​-             >
    ​​​-               {todo.name}
    ​​​-             </p>
    ​​​-           </td>
    ​​​-           <td></td>
    ​​​-           <td className="py-4 px-6">
    ​​​-             <button
    ​​​-               onClick={() => remove(todo.id)}
    ​​​-               className="py-2 px-4 font-bold text-white bg-red-500 rounded"
    ​​​-             >
    ​​​-               削除
    ​​​-             </button>
    ​​​-           </td>
    ​​​-         </tr>
    ​​​  ))}
    ​​​</tbody>
    

    切り取った部分を、Todo コンポーネントに貼り付けます。

    pages/index.tsx

    ​​​const Todo = () => {
    ​​​  return (
    ​​​+   <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​+     <td className="py-4 px-6">
    ​​​+       <input
    ​​​+         type="checkbox"
    ​​​+         checked={todo.isDone}
    ​​​+         onChange={() => toggle(todo.id)}
    ​​​+       />
    ​​​+     </td>
    ​​​+     <td className="py-4 px-6">
    ​​​+       <p
    ​​​+         className="w-40"
    ​​​+         style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​+       >
    ​​​+         {todo.name}
    ​​​+       </p>
    ​​​+     </td>
    ​​​+     <td></td>
    ​​​+     <td className="py-4 px-6">
    ​​​+       <button
    ​​​+         onClick={() => remove(todo.id)}
    ​​​+         className="py-2 px-4 font-bold text-white bg-red-500 rounded"
    ​​​+       >
    ​​​+         削除
    ​​​+       </button>
    ​​​+     </td>
    ​​​+   </tr>
    ​​​  );
    ​​​};
    

    todo remove toggle の部分にでエラーが出ていたら正解です。

    エラーが出ているのは、Todo コンポーネントの中でtodo remove toggleを呼び出しても、Todo コンポーネント内にこれらに対応するものがないからです。今から、親コンポーネント(Home)から子コンポーネント(Todo)へ、todo remove toggle が渡せるようにします。(詳しくは解説で)

  2. Todo コンポーネントのインターフェイスを作る TodoProps という名前のインターフェイスを作ります。

    pages/index.tsx

    ​​​+ interface Props {
    ​​​+ todo: Todo
    ​​​+ onToggle: (id: number) => void
    ​​​+ onRemove: (id: number) => void
    ​​​+ }
    
    ​​​const Todo = () => {
    

    次に、以下のように修正してください。

    pages/index.tsx

    ​​​- const Todo = () => {
    ​​​+ const Todo = ({ todo, onToggle, onRemove }: TodoProps) => {
    

    return の中も、以下のように修正してください。先程までのエラーが消えるはずです。

    pages/index.tsx

    ​​​return (
    ​​​  <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​    <td className="py-4 px-6">
    ​​​-      <input type="checkbox" checked={todo.isDone} onChange={() => toggle(todo.id)} />
    ​​​+      <input type="checkbox" checked={todo.isDone} onChange={() => onToggle(todo.id)} />
    ​​​    </td>
    ​​​    <td className="py-4 px-6">
    ​​​      <p
    ​​​        className="w-40"
    ​​​        style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​      >
    ​​​        {todo.name}
    ​​​      </p>
    ​​​    </td>
    ​​​    <td className="py-4 px-6"></td>
    ​​​    <td className="py-4 px-6">
    ​​​      <button
    ​​​-        onClick={() => remove(todo.id)}
    ​​​+        onClick={() => onRemove(todo.id)}
    ​​​        className="py-2 px-4 font-bold text-white bg-red-500 hover:bg-red-700 rounded"
    ​​​      >
    ​​​        削除
    ​​​      </button>
    ​​​    </td>
    ​​​  </tr>
    ​​​);
    
  3. Home コンポーネントの中で Todo コンポーネントを呼び出す ローカルホストで挙動を確認して下しさい。 pages/index.tsx

    ​​​<tbody>
    ​​​  {todoList.map((todo) => (
    ​​​+    <Todo
    ​​​+      key={todo.id}
    ​​​+      todo={todo}
    ​​​+      onRemove={remove}
    ​​​+      onToggle={toggle}
    ​​​+    />
    ​​​  ))}
    ​​​</tbody>
    

解説

コンポーネント

コンポーネントは、特別な JavaScript の関数です。特別な点は 2 つです。

  1. 関数名が大文字から始まる
  2. JSX を返す
const 大文字で始める関数名 = (引数) => {

  return (
    <p>JSXを返す</p>;
  )
};

関数のスコープ

スコープとは、変数の名前や関数などの参照できる範囲を決めるものです。 スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません。例えば、関数の中で定義された変数は、その関数の中でしか参照することしかできないことになっています。

今回の実装を振り返ります。実装手順 1 の終わりでエラーが出ていたのは、Home コンポーネントの変数がスコープ外である Todo コンポーネントから呼び出されていたからです。コンポーネントは関数でしたね。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

親コンポーネントから子コンポーネントに Props を渡す

コンポーネントは親から子へ Props として変数や関数、JSX などあらゆる情報をわたすことができます。今回の実装を確認します。Home コンポーネントから Todo コンポーネントを呼び出しました。このとき、Home コンポーネントが親、Todo コンポーネントが子になります。

pages/index.tsx

<tbody>
  {todoList.map((todo) => (
    <Todo key={todo.id} todo={todo} onRemove={remove} onToggle={toggle} />
  ))}
</tbody>

Props は、todoonRemoveonToggleになります。例えば、 Home コンポーネントから Todo コンポーネントの onRemove に渡された remove は、Todo コンポーネントの中でonRemoveとして呼び出すことが可能になります。

Todo コンポーネントを確認します。削除ボタンの onClick 内の処理でonRemove が呼び出されています。

pages/index.tsx

const Todo = ({ todo, onToggle, onRemove }: Props) => {
  s;
  return (
    <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
      <td className="py-4 px-6">
        <input
          type="checkbox"
          checked={todo.isDone}
          onChange={() => onToggle(todo.id)}
        />
      </td>
      <td className="py-4 px-6">
        <p
          className="w-40"
          style={{ textDecoration: todo.isDone ? "line-through" : "" }}
        >
          {todo.name}
        </p>
      </td>
      <td className="py-4 px-6">
        <button
          onClick={() => onRemove(todo.id)}
          className="py-2 px-4 font-bold text-white bg-red-500 hover:bg-red-700 rounded"
        >
          削除
        </button>
      </td>
    </tr>
  );
};

インターフェイス

インターフェイスとは、Props の型の集合です。インターフェイスを定義することで、子コンポーネントが親から受け取る Props の型を定義できます。実装を確認します。TodoProps は、「todo は Todo」「onToggle は number 型の引数を受け取り void を返す関数」「onRemove は number 型の引数を受け取り void を返す関数」という Props の型定義の集合です。

pages/index.tsx

interface TodoProps {
  todo: Todo;
  onToggle: (id: number) => void
  onRemove: (id: number) => void
}

const Todo = ({ todo, onToggle, onRemove }: TodoProps) => {
  return (
    // 省略
  );
};

Todo を編集モードに切り替えられるようにする

このセクションと次のセクションで、既存の Todo を編集する機能を実装します。このセクションでは、既存の Todo を編集モードに切り替えられるようにします。

実装手順

  1. 編集モードを作成する

    pages/index.tsx

    ​​​const Todo = ({ todo, onToggle, onRemove, onEdit }: TodoProps) => {
    ​​​+  const [isEditing, setIsEditing] = useState(false)
    
    ​​​+  const startEditing = () => setIsEditing(true)
    ​​​+  const finishEditing = () => setIsEditing(false)
    

    pages/index.tsx

    ​​​return (
    ​​​+ <>
    ​​​+   {isEditing ? (
    ​​​+     <tr className="bg-white dark:bg-gray-800 border-b">
    ​​​+       <td></td>
    ​​​+       <td className="py-4 px-6">
    ​​​+         <input
    ​​​+           type="text"
    ​​​+           className="border"
    ​​​+           value={todo.name}
    ​​​+         />
    ​​​+       </td>
    ​​​+       <td className="py-4 px-6">
    ​​​+         <button
    ​​​+           onClick={finishEditing}
    ​​​+           className="py-2 px-4 font-bold text-white bg-green-500 hover:bg-green-700 rounded"
    ​​​+         >
    ​​​+           編集終了
    ​​​+         </button>
    ​​​+       </td>
    ​​​+       <td className="py-4 px-6">
    ​​​+         <button
    ​​​+           onClick={() => onRemove(todo.id)}
    ​​​+           className="py-2 px-4 font-bold text-white bg-red-500 rounded opacity-50 cursor-not-allowed"
    ​​​+           disabled
    ​​​+         >
    ​​​+           削除
    ​​​+         </button>
    ​​​+       </td>
    ​​​+     </tr>
    ​​​+   ) : (
    ​​​     <tr key={todo.id} className="bg-white dark:bg-gray-800 border-b">
    ​​​       <td className="py-4 px-6">
    ​​​         <input type="checkbox" checked={todo.isDone} onChange={() => onToggle(todo.id)} />
    ​​​       </td>
    ​​​       <td className="py-4 px-6">
    ​​​         <p
    ​​​           className="w-40"
    ​​​           style={{ textDecoration: todo.isDone ? "line-through" : "" }}
    ​​​         >
    ​​​           {todo.name}
    ​​​         </p>
    ​​​       </td>
    ​​​       <!-- 省略 -->
    ​​​     </tr>
    ​​​+   )}
    ​​​+ </>
    ​​​)
    

解説

UI の条件分岐

三項演算子を使って、UI を分岐させることができます。実装を確認します。

pages/index.tsx

return (
  <>
    {isEditing ? (
      <li>Todo編集モード=isEditingがtrueのときのUI</li>
    ) : (
      <li>Todo表示モード=isEditingがfalseのときのUI</li>
    )}
  </>
);

まとめ

  1. 編集ボタンを押すと、編集モードに見た目が切り替わる
  2. 編集終了ボタンを押すと、表示モードに切り替わる

Todo を編集できるようにする

このセクションでは、編集モードで入力した内容がアプリに反映されるようにします。

実装手順

  1. 反映する関数を作り、Prop として Todo コンポーネントにわたす まず、 Home コンポーネント内に edit を作ります。

    pages/index.tsx

    ​​​const clear = () => {
    ​​​  // 省略
    ​​​};
    
    ​​​+ const edit = (id: number) => (name: string) => {
    ​​​+   const newState = todoList.map((todo) => {
    ​​​+     if (todo.id !== id) return todo;
    ​​​+     return { ...todo, name: name };
    ​​​+   });
    ​​​+
    ​​​+   setTodoList(newState);
    ​​​+ };
    

    edit を Todo コンポーネントに渡せるように、インターフェイスを変更します。

    pages/index.tsx

    ​​​interface TodoProps {
    ​​​  todo: Todo
    ​​​  onToggle: (id: number) => void
    ​​​  onRemove: (id: number) => void
    ​​​+  onEdit: (id: number, name: string) => void
    ​​​}
    
    ​​​- const Todo = ({ todo, onToggle, onRemove }: TodoProps) => {
    ​​​+ const Todo = ({ todo, onToggle, onRemove, onEdit }: TodoProps) => {
    

    edit を Todo コンポーネントの onEdit に渡します。

    pages/index.tsx

    ​​​<Todo
    ​​​  key={todo.id}
    ​​​  todo={todo}
    ​​​  onRemove={remove}
    ​​​  onToggle={toggle}
    ​​​+  onEdit={edit}
    ​​​/>
    
  2. 編集フォームへの入力内容が反映されるようにする

    pages/index.tsx

    ​​​const Todo = ({ todo, onToggle, onRemove, onEdit }: TodoProps) => {
    ​​​  const [isEditing, setIsEditing] = useState(false)
    
    ​​​+  const handleChangeInput = (e: React.ChangeEvent<HTMLInputElement>) => {
    ​​​+    onEdit(e.target.value)
    ​​​+  }
    

    pages/index.tsx

    ​​​ {isEditing ? (
    ​​​ <tr className="bg-white dark:bg-gray-800 border-b">
    ​​​   <td className="py-4 px-6"></td>
    ​​​   <td className="py-4 px-6">
    ​​​     <input
    ​​​       type="text"
    ​​​ +      onChange={handleChangeInput}
    ​​​       className="border"
    ​​​       value={todo.name}
    ​​​     />
    ​​​   </td>
    

参考文献