ToDo一覧の絞り込み

ToDoページで表示されるToDoを絞り込みできるように実装します。

stateの設計

ToDoの表示対象を絞り込むために、どのようなstateが必要になるかを考えていきます。

ボタンを押すとどれか1つが選択された状態にするため、どのボタンが選択されているかのstateは必要そうです。

他に、絞り込んだ後のToDoの一覧は、stateとして保持する必要があるかを考えます。これは、TodoListで使用している全てのToDoを保持するstateと、どのボタンが選択されているかのstateで算出可能なため、stateにはなりません。(参考:React の流儀 ステップ 3 – React

全てのToDoを保持するstateがTodoBoardにあること、TodoBoardTodoListTodoFilterの共通の親であることから、どのボタンが選択されているのかのstateもTodoBoardに定義します。こうすることで、TodoBoardが全体の状態を一元管理することができ、メンテナンスが容易になります。

コンポーネントの実装

コンポーネントの実装では、TypeScriptの型を利用した実装にしていきます。

TodoFilterの実装

表示対象を選択するボタンを表示するTodoFilterコンポーネントを実装します。stateの設計であったとおり、現在どのボタンが押されているかをTodoBoardにstateとして保持させます。そのため、ここではプロパティでstateと更新関数を受け取るようにします。

また、TodoBoardで管理した場合、TodoBoardでもどういったボタンがあるのかを知っている必要があります。ただし、どういうボタンがあるのかはTodoFilterが決めるため、TodoFilterで選択可能な種類をTodoBoardに知らせる型を宣言します。

src/components/board/filter/TodoFilter.tsx

import React from 'react';
import styles from './TodoFilter.module.css';

export type FilterType = 'ALL' | 'INCOMPLETE' | 'COMPLETED';

type Props = {
  filterType: FilterType;
  setFilterType: (filter: FilterType) => void;
};

export const TodoFilter: React.FC<Props> = ({filterType, setFilterType}) => {
  return (
    <div className={styles.content}>
      <button
        className={filterType === 'ALL' ? `${styles.buttonSelected}` : `${styles.buttonUnselected}`}
        disabled={filterType === 'ALL'}
        onClick={() => setFilterType('ALL')}
      >
        全て
      </button>
      <button
        className={filterType === 'INCOMPLETE' ? `${styles.buttonSelected}` : `${styles.buttonUnselected}`}
        disabled={filterType === 'INCOMPLETE'}
        onClick={() => setFilterType('INCOMPLETE')}
      >
        未完了のみ
      </button>
      <button
        className={filterType === 'COMPLETED' ? `${styles.buttonSelected}` : `${styles.buttonUnselected}`}
        disabled={filterType === 'COMPLETED'}
        onClick={() => setFilterType('COMPLETED')}
      >
        完了のみ
      </button>
    </div>
  );
};

このコンポーネントで扱うボタンの種類を知らせるため、次のようにFilterTypeという型を宣言し、exportしています。

export type FilterType = 'ALL' | 'INCOMPLETE' | 'COMPLETED';

これはUnion Types(共用体型)と呼ばれるTypeScriptの型で、型を|で区切ると、その型のどれか(or条件)が代入可能になります。(参考:TypeScript - Union Types

ここでは、このUnion Typesと固定の文字列を組み合わせることで、FilterType型にはこの文字列のどれかのみ代入することを可能にしています。 TodoBoardではこの型によって、TodoFilterが受け入れられる値を確実に使用できます。もしこれらの文字列以外を代入しようとするとコンパイルエラーになるため、
TodoBoard側のミスを防ぎ、さらにFilterTypeで種類が変わった際に問題を検知できるようになります。

TodoBoardの実装

続いて、TodoBoardを実装します。ここでは、先ほど実装したFilterType型のstateを配置し、現在どれが選択されているかを保持するようにします。
現在保持されているFilterTypeのstateによりTodoListにToDoデータを渡す前にフィルタ処理を行い、絞り込んだ後のToDoデータをTodoListのプロパティに渡すようにします。

src/components/board/TodoBoard.tsx

...
import {FilterType, TodoFilter} from './filter/TodoFilter';
...

type ShowFilter = {
  [K in FilterType]: (todo: Todo) => boolean;
};

const showFilter: ShowFilter = {
  ALL: () => true,
  INCOMPLETE: todo => !todo.completed,
  COMPLETED: todo => todo.completed,
};

export const TodoBoard: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filterType, setFilterType] = useState<FilterType>('ALL');
...
  const showTodos = todos.filter(showFilter[filterType]);

  return (
    <div className={styles.content}>
      <TodoForm addTodo={addTodo} />
      <TodoFilter filterType={filterType} setFilterType={setFilterType} />
      <TodoList todos={showTodos} toggleTodoCompletion={toggleTodoCompletion} />
    </div>
  );
};

ここでは、TodoFilterが定義しているFilterType型に対応したフィルタ処理を必ず宣言するように、次のような型を定義しています。

type ShowFilter = {
  [K in FilterType]: (todo: Todo) => boolean;
};

これは、Mapped typesと呼ばれるTypeScriptの型で、オブジェクトのプロパティ名や値の型を制限することができます。[K in FilterType]部分はオブジェクトのプロパティ名の型を表しています。Kは型引数であり、ここではFilterTypeに含まれる文字列のどれかがプロパティ名として使用できるようにしています。: (todo: Todo) => boolean部分は、プロパティ値の型を表しています。

この型を使い、ボタンの種類に対応するフィルタ関数を定義したオブジェクトを生成しています。

const showFilter: ShowFilter = {
  ALL: () => true,
  INCOMPLETE: todo => !todo.completed,
  COMPLETED: todo => todo.completed,
};

先ほどの型により、ここでのプロパティ名はFilterTypeに代入可能な文字列のどれかである必要があり、それ以外のプロパティ名であればコンパイルエラーになります。そのため、確実にボタンの種類に対応しているフィルタ関数を宣言されるようにしています。

ここで定義したFilterTypeに対応した関数を使用するため、次のように宣言しています。現在のstateに応じたフィルタ関数が取得され、filterに適用されるようにしています。

const showTodos = todos.filter(showFilter[filterType]);

モックを使用した動作確認

ToDoページを表示して、ボタンをクリックしてToDo一覧を絞り込んで表示できることを確認します。

確認できたら、フロントエンドのToDo一覧の絞り込みの実装は完了です。

results matching ""

    No results matching ""