ToDoの登録

ToDoページで新しいToDoを登録できるように実装します。

stateの設計

ToDoの一覧表示の時と同様に、ToDoを登録するコンポーネントで、どのような状態が必要になるかを考えていきます。

ここでは、ToDoを登録するためのフォームを作成します。フォームの作成についてはReactからガイドされており、フォーム自身が保持する独自のstateをReactが管理するstateに統合し、Reactのstateのみで制御する「制御されたコンポーネント」として作成することを推奨しています。
(参考:inputselecttextarea

ここからわかるとおり、フォームでは入力中の状態を保持するstateが必要になります。ここでは、ToDoの内容を入力するフォームであるため、入力中のToDoの内容を保持するstateが必要になります。

このstateは、ToDoを登録するコンポーネントであるTodoFormでしか必要ないため、stateはこのコンポーネントに配置します。

登録完了したToDoは一覧に表示しますが、一覧で表示するためのToDoを保持するstateはTodoBoardに配置しています。そのため、TodoFormでToDoを登録した際にTodoBoardのstateを更新するようにします。

ToDo登録フォームの実装

TodoFormを実装していきます。このコンポーネントにあるフォームには、テキスト入力ボックスがあります。これを先ほど説明した「制御されたコンポーネント」として実装します。

このようなテキスト入力ボックスを実装するために、example-chatのフロントエンドではuseInputという独自のフックを実装していますので、その実装を流用します。

まず、src/hooksディレクトリを作成し、そこにuseInput.tsファイルを作成します。 example-chatのsrc/framework/hooks/index.tsuseInputが定義されているため、このコードをuseInput.tsファイルに持ってきます。

src/hooks/useInput.ts

import {useState} from 'react';

/**
 * input要素のステートフックとステート更新をラッピングした独自フック。
 *
 * @param initialState 初期値
 * @return [input要素のステート, input要素の属性, ステート更新の関数]
 */
export const useInput = (
  initialState: string = '',
): [string, React.InputHTMLAttributes<HTMLInputElement>, React.Dispatch<React.SetStateAction<string>>] => {
  const [value, setValue] = useState<string>(initialState);

  const onChange = (event: React.FormEvent<HTMLInputElement>) => {
    setValue(event.currentTarget.value);
  };

  return [
    value,
    {
      value,
      onChange,
    },
    setValue,
  ];
};

TodoFormでは、このuseInputを使用して、次のように実装します。

src/components/board/form/TodoForm.tsx

import React from 'react';
import styles from './TodoForm.module.css';
import {useInput} from '../../../hooks/useInput';

export const TodoForm: React.FC = () => {
  const [text, textAttributes, setText] = useInput('');

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    // 登録した際の処理を書く予定だが、この時点で動作確認しやすいように一旦 alert で入力値を表示する
    alert(text);
  };

  return (
    <div className={styles.content}>
      <form onSubmit={handleSubmit} className={styles.form}>
        <div className={styles.input}>
          <input type='text' {...textAttributes} placeholder='タスクを入力してください' />
        </div>
        <div className={styles.button}>
          <button type='submit'>追加</button>
        </div>
      </form>
    </div>
  );
};

useInputでは、useStateと同様に呼び出し時に初期値を渡します。戻り値としては、state自体やinputに渡すためのプロパティが設定されたオブジェクト等が返されます。

inputのプロパティを個別に設定してもよいですが、ここではスプレッド構文を使用して、そのオブジェクトに設定されているプロパティを展開して一気に設定します。

<input type='text' {...textAttributes} placeholder='タスクを入力してください' />

textAttributesにはvalueonChangeプロパティがあるため、これは次の実装と同じ意味になります。

<input type='text' value={textAttributes.value} onChange={textAttributes.onChange} placeholder='タスクを入力してください' />

フォームのサブミットで登録処理を行うように、「追加」ボタンのtypesubmitに設定します。これで、「追加」ボタンをクリックするとサブミットされるようになります。

サブミット時に登録処理を実行するため、登録処理をhandleSubmit関数として実装し、
formonSubmitに設定します。これで、サブミット時にこの関数がコールバックされます。 また、サブミット時に関数がコールバックされた後、そのままだとサブミットイベントによりフォームをサーバに送信しようとしてしまうので、次のように関数内でevent.preventDefault()を呼び、サブミットイベントをキャンセルしておきます。

  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
...

サブミットイベントをキャンセルすると何も起きなくなるため、この時点で動作確認をしたい場合には少し分かりづらくなります。そのため、ここでは一旦 alert で入力値を表示するようにしています。これはあくまで一時的な確認用のため、この後でREST APIを呼び出す処理を実装する際には削除します。

REST APIの呼び出しとstateの更新

先ほど作成したサブミット用のコールバック関数では、イベントをキャンセルするだけでしたが、生成したクライアントコードでREST APIを呼び出すように実装していきます。

REST APIを呼び出すと登録した結果のToDoがレスポンスとして返されるので、それを一覧に表示されるようにします。ただ、一覧に表示するためのstateは、TodoFormの親コンポーネントであるTodoBoardに配置しているため、TodoFormの実装では更新することができません。このような場合、stateを配置しているコンポーネントでstateを更新するためのコールバック関数を定義し、それをプロパティで渡してもらうようにします。

また、ToDoを登録した後は入力したテキストもクリアさせるため、テキスト入力のstateも更新します。 async/awaitを使うことで、非同期処理の結果を受け取るまで実行を待機します。この方法により、フォームのテキストが確実にサーバーに送られてレスポンスを受け取った後にクリアされるため、データの消失を防ぐことができます。

import React from 'react';
import styles from './TodoForm.module.css';
import {useInput} from '../../../hooks/useInput';
import {BackendService} from '../../../backend/BackendService';

type Todo = {
  id: number;
  text: string;
  completed: boolean;
};

type Props = {
  addTodo: (todo: Todo) => void;
};

export const TodoForm: React.FC<Props> = ({addTodo}) => {
  const [text, textAttributes, setText] = useInput('');

  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    if (!text) {
      return;
    }
    const response = await BackendService.postTodo(text);
    addTodo(response);
    setText('');
  };

  return (
    <div className={styles.content}>
      <form onSubmit={handleSubmit} className={styles.form}>
        <div className={styles.input}>
          <input type='text' {...textAttributes} placeholder='タスクを入力してください' />
        </div>
        <div className={styles.button}>
          <button type='submit'>追加</button>
        </div>
      </form>
    </div>
  );
};

TodoBoardでは、次のように、引数のToDoをstateのToDoに結合させる関数を作成し、
それをTodoFormに渡すように実装します。

src/components/board/TodoBoard.tsx

export const TodoBoard: React.FC = () => {
...
  const addTodo = (todo: Todo) => {
    setTodos(todos.concat(todo));
  };
...
  return (
    <div className={styles.content}>
      <TodoForm addTodo={addTodo} />
...

このようにすることで、TodoBoardのstateの管理はTodoBoardに残したまま、TodoFormから更新することができるようになります。

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

ToDoページを表示して、ToDoが登録できることを確認します。ToDo登録フォームのテキスト入力に適当な値を入力して「追加」ボタンをクリックし、モックサーバからのレスポンスで取得したToDoが一覧に追加されるのを確認します。

モックサーバの起動時にも説明しましたが、モックサーバからはOpenAPIドキュメントのexampleに設定した値がレスポンスとして返されます。そのため、ここでは常に同じToDo(やること3)が追加されることになり、実際に入力したToDoが追加されるわけではありませんので、注意してください。

確認ができたら、フロントエンドのToDoの登録の実装は完了です。

results matching ""

    No results matching ""