Chapter 14無料公開

タスクをフィルタリングする機能を追加する

Kei Touge
Kei Touge
2021.11.22に更新

このままでは、完了済みアイテムや削除済みアイテムもいっしょにそのまま表示されてしまうので、タスクをフィルタリングする機能を追加します。

フィルタリングするセレクタを作成

ここでも onChange イベントへはとりあえずダミーを与えておきます。

src/App.tsx
    <div>
      <select defaultValue="all" onChange={(e) => e.preventDefault()}>
        <option value="all">すべてのタスク</option>
        <option value="checked">完了したタスク</option>
        <option value="unchecked">現在のタスク</option>
        <option value="removed">ごみ箱</option>
      </select>
      <form onSubmit={(e) => handleOnSubmit(e)}>
~ snip ~
    </div>

現在のフィルターを格納する filter ステートを追加する

フィルターの状態をあらわす Filter 型を新設し、 その種別は4種類とします。

src/App.tsx
type Filter = 'all' | 'checked' | 'unchecked' | 'removed';
フィルター タスクの種別
all すべてのタスク(削除済みのタスクをのぞく)
checked 完了したタスク
unchecked 現在の(未完了の)タスク
removed ごみ箱(削除済みのタスク)

前項の <option /> タグの値を Filter 型のステート として保持しましょう。

src/App.tsx
export const App = () => {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState<Todo[]>([]);
  // 追加
  const [filter, setFilter] = useState<Filter>('all');

上のセレクタの値が変化 (onChange イベントの発火)すると filter ステートを更新させるようにします。

Filter を単なる string 型 にすれば下のようなキャスト(=型変換)は不要ですが、次項の switch 文で型によるエディタの補完を享受するため、あえて Filter 型 を適用しています。

src/App.tsx
      // e.target.value: string を Filter 型にキャストする
      <select
        defaultValue="all"
        onChange={(e) => setFilter(e.target.value as Filter)}
      >
        <option value="all">すべてのタスク</option>
        <option value="checked">完了したタスク</option>
        <option value="unchecked">現在のタスク</option>
        <option value="removed">ごみ箱</option>
      </select>

https://developer.mozilla.org/ja/docs/Glossary/Type_Conversion

フィルタリング後の Todo 型の配列をリスト表示する

todos ステート 配列の表示方法を変化させる関数を作成しましょう。

  • <ul></ul> タグの中で展開されている todos ステート をタグへ渡す前に加工する
  • 現在の filter ステート に応じて Todo 型配列 の要素をフィルタリングする
  • Array.prototype.filter() メソッドも非破壊メソッドかつシャローコピー
  • Todo 型オブジェクト内のプロパティを編集するわけではないので、イミュータビリティには影響がない

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
src/App.tsx
  const filteredTodos = todos.filter((todo) => {
    // filter ステートの値に応じて異なる内容の配列を返す
    switch (filter) {
      case 'all':
        // 削除されていないもの全て
        return !todo.removed;
      case 'checked':
        // 完了済 **かつ** 削除されていないもの
        return todo.checked && !todo.removed;
      case 'unchecked':
        // 未完了 **かつ** 削除されていないもの
        return !todo.checked && !todo.removed;
      case 'removed':
        // 削除済みのもの
        return todo.removed;
      default:
        return todo;
    }
  });

todos ステートを展開する <ul></ul> タグにフィルタリング済みのリストを渡すように書き換えます。

src/App.tsx
        <ul>
 -         {todos.map((todo) => {
+         {filteredTodos.map((todo) => {
            return (
              <li key={todo.id}>
                <input
                  type="checkbox"
                  disabled={todo.removed}
 

「ごみ箱」「完了済みのタスク」が表示されている時は、あらたなタスクを追加できないように入力フォームは無効化しましょう。

src/App.tsx
      <form onSubmit={(e) => handleOnSubmit(e)}>
        <input
          type="text"
          value={text}
          disabled={filter === 'checked' || filter === 'removed'}
          onChange={(e) => handleOnChange(e)}
        />
        <input
          type="submit"
          value="追加"
          disabled={filter === 'checked' || filter === 'removed'}
          onSubmit={(e) => handleOnSubmit(e)}
        />
      </form>

2021-04-25-122246.png

この章のソースコード全文 App.tsx
src/App.tsx
import { useState } from 'react';

type Todo = {
  value: string;
  readonly id: number;
  checked: boolean;
  removed: boolean;
};

type Filter = 'all' | 'checked' | 'unchecked' | 'removed';

export const App = () => {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<Filter>('all');

  const handleOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const handleOnSubmit = () => {
    if (!text) return;

    const newTodo: Todo = {
      value: text,
      id: new Date().getTime(),
      checked: false,
      removed: false,
    };

    setTodos([newTodo, ...todos]);
    setText('');
  };

  const handleOnEdit = (id: number, value: string) => {
    const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos));

    const newTodos = deepCopy.map((todo) => {
      if (todo.id === id) {
        todo.value = value;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const handleOnCheck = (id: number, checked: boolean) => {
    const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos));

    const newTodos = deepCopy.map((todo) => {
      if (todo.id === id) {
        todo.checked = !checked;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const handleOnRemove = (id: number, removed: boolean) => {
    const deepCopy: Todo[] = JSON.parse(JSON.stringify(todos));

    const newTodos = deepCopy.map((todo) => {
      if (todo.id === id) {
        todo.removed = !removed;
      }
      return todo;
    });

    setTodos(newTodos);
  };

  const filteredTodos = todos.filter((todo) => {
    switch (filter) {
      case 'all':
        return !todo.removed;
      case 'checked':
        return todo.checked && !todo.removed;
      case 'unchecked':
        return !todo.checked && !todo.removed;
      case 'removed':
        return todo.removed;
      default:
        return todo;
    }
  });

  return (
    <div>
      <select
        defaultValue="all"
        onChange={(e) => setFilter(e.target.value as Filter)}
      >
        <option value="all">すべてのタスク</option>
        <option value="checked">完了したタスク</option>
        <option value="unchecked">現在のタスク</option>
        <option value="removed">ごみ箱</option>
      </select>
      <form
        onSubmit={(e) => {
          e.preventDefault();
          handleOnSubmit();
        }}
      >
        <input
          type="text"
          value={text}
          disabled={filter === 'checked' || filter === 'removed'}
          onChange={(e) => handleOnChange(e)}
        />
        <input
          type="submit"
          value="追加"
          disabled={filter === 'checked' || filter === 'removed'}
          onSubmit={handleOnSubmit}
        />
      </form>
      <ul>
        {filteredTodos.map((todo) => {
          return (
            <li key={todo.id}>
              <input
                type="checkbox"
                disabled={todo.removed}
                checked={todo.checked}
                onChange={() => handleOnCheck(todo.id, todo.checked)}
              />
              <input
                type="text"
                disabled={todo.checked || todo.removed}
                value={todo.value}
                onChange={(e) => handleOnEdit(todo.id, e.target.value)}
              />
              <button onClick={() => handleOnRemove(todo.id, todo.removed)}>
                {todo.removed ? '復元' : '削除'}
              </button>
            </li>
          );
        })}
      </ul>
    </div>
  );
};