サインアップ

ユーザーコンテクストを使用して、サインアップ処理を実装します。

サインアップ処理の実装

Signupコンポーネントにサインアップ処理を実装します。

src/components/signup/Signup.tsx

import React from 'react';
import {useRouter} from 'next/navigation';
import styles from './Signup.module.css';
import {useInput} from '../../hooks/useInput';
import {useUserContext} from '../../contexts/UserContext';

export const Signup: React.FC = () => {
  const router = useRouter();
  const [userName, userNameAttributes] = useInput('');
  const [password, passwordAttributes] = useInput('');
  const userContext = useUserContext();

  const signup: React.FormEventHandler<HTMLFormElement> = async event => {
    event.preventDefault();
    await userContext.signup(userName, password);
    router.push('/');
  };

  return (
    <div className={styles.content}>
      <div className={styles.box}>
        <div className={styles.title}>
          <h1>ユーザー登録</h1>
        </div>
        <form className={styles.form} onSubmit={signup}>
          <div className={styles.item}>
            <div className={styles.label}>名前</div>
            <input type='text' {...userNameAttributes} />
          </div>
          <div className={styles.item}>
            <div className={styles.label}>パスワード</div>
            <input type='password' {...passwordAttributes} />
          </div>
          <div className={styles.buttonGroup}>
            <button type='submit' className={styles.button}>
              登録する
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

フォームには名前とパスワードのテキストボックスがあります。ToDo登録と同様にuseInputを使用してstateとプロパティ用オブジェクトを作成し、フォームのJSXで各項目のプロパティとして展開します。

フォームのサブミット時には、ユーザーコンテクストのサインアップ関数に入力値を渡して実行するようにします。

入力値のバリデーション

ToDoを登録する際には入力値のバリデーションを行いませんでしたが、ここでは入力値に対して次のバリデーションを実装します。

  • 名前が入力されているか
  • パスワードが入力されているか
  • パスワードが4文字以上であるか

example-chatのフロントエンドでもこのような入力値のバリデーションを実装していますので、その実装を流用します。(starter-kitに元から用意しています。)

src/validationディレクトリの中に入力値のバリデーションを実装したファイルが格納されています。

バリデーション用のフックであるuseValidationが実装されているため、これを使用してSignupでバリデーションを実装します。

...
import {stringField, useValidation} from '../../validation';

type ValidationFields = {
  userName: string;
  password: string;
};

export const Signup: React.FC = () => {
...
  const {error, handleSubmit} = useValidation<ValidationFields>({
    userName: stringField().required('名前を入力してください'),
    password: stringField()
      .required('パスワードを入力してください')
      .minLength(4, 'パスワードは4桁以上入力してください'),
  });
...
  return (
    <div className={styles.content}>
      <div className={styles.box}>
        <div className={styles.title}>
          <h1>ユーザー登録</h1>
        </div>
        <form className={styles.form} onSubmit={handleSubmit({userName, password}, signup)}>
          <div className={styles.item}>
            <div className={styles.label}>名前</div>
            <input type='text' {...userNameAttributes} />
            <div className='error'>{error.userName}</div>
          </div>
          <div className={styles.item}>
            <div className={styles.label}>パスワード</div>
            <input type='password' {...passwordAttributes} />
            <div className='error'>{error.password}</div>
          </div>
          <div className={styles.buttonGroup}>
            <button type='submit' className={styles.button}>
              登録する
            </button>
          </div>
        </form>
      </div>
    </div>
  );
};

ここではエラーメッセージのスタイル用に、クラス名としてerrorを使用します。これは他のコンポーネントでも使用するため、globals.cssに次のスタイル定義を追加します。

src/app/globals.css

.error {
  color: red;
}

useValidationでは、型引数により各項目で使用できるバリデーションを切り替えます。そのため、まずはバリデーション対象になる項目を定義したValidationFields型を定義します。

useValidationの型引数にValidationFieldsを指定し、引数にバリデーションを定義したオブジェクトを生成して渡します。プロパティのキーはValidationFieldsに対応し、各項目でどのようなバリデーションを行うかを定義します。

string型の項目を検証するためのstringFieldが用意されているので、これを起点にバリデーションを指定していきます。stringFieldでは、必須入力をチェックするためのrequiredや最小文字数をチェックするためのminLength等が用意されており、ここでのバリデーションにはそれらを使用します。

useValidationの戻り値として、バリデーションエラーが格納されるerrorとサブミット時に自動で実行するためのhandleSubmitを受け取ります。

errorは実際にはuseStateによるstateであり、バリデーションでエラーになるとこのstateにメッセージが設定されます。errorのプロパティはuseValidationのプロパティに一対一で対応しています。例えば、userNameに対するバリデーションでエラーとなった場合、error.userNameにエラーメッセージが設定されます。

ここでは、各テキストボックスの下にエラーメッセージを表示するように、JSXでinputの直後にdivでエラーメッセージを表示するようにします。

handleSubmitは関数であり、第1引数に入力値を渡し、第2引数にエラーが発生しなかった場合のコールバック関数を渡します。

サインアップ失敗時のハンドリング

サインアップのREST APIでは、登録する名前と同じ名前がすでに登録されている場合、ステータスコードが409であるエラーレスポンスが返却されます。

ユーザーコンテクストのサインアップ関数を実装した際、そのエラーレスポンスであれば
AccountConflictErrorクラスのインスタンスを返却するように実装しました。そのため、ここではサインアップ関数の実行結果からエラーであるかを判断し、エラーであればフォームの上にエラーメッセージを表示するようにします。

useValidationではこのようなサーバサイドのエラーについてはハンドリングできないため、エラーメッセージを表すstateを新しく作成し、次のように実装します。

import React, {useState} from 'react';
...
import {AccountConflictError, useUserContext} from '../../contexts/UserContext';
...

export const Signup: React.FC = () => {
...
  const [formError, setFormError] = useState('');
...
  const signup: React.FormEventHandler<HTMLFormElement> = async event => {
    event.preventDefault();
    const result = await userContext.signup(userName, password);
    if (result instanceof AccountConflictError) {
      setFormError('サインアップに失敗しました。同じ名前が登録されています。');
      return;
    }
    router.push('/');
  };

  return (
    <div className={styles.content}>
      <div className={styles.box}>
        <div className={styles.title}>
          <h1>ユーザー登録</h1>
          <div className='error'>{formError}</div>
        </div>
        <form className={styles.form} onSubmit={handleSubmit({userName, password}, signup, () => setFormError(''))}>
...
        </form>
      </div>
    </div>
  );
};

サインアップ関数の戻り値がAccountConflictErrorクラスのインスタンスであれば、サインアップ失敗としてエラーメッセージをstateに設定します。フォームの上にエラーメッセージを表示するように、
JSXでformの直前にdivでエラーメッセージを表示するようにします。

なお、フォーム上のエラーメッセージが表示された状態で再度フォームに入力し、入力値のバリデーションでエラーとなった場合、フォーム上のエラーメッセージは非表示にします。
そのために、handleSubmitの第3引数として、バリデーション実行前にエラーメッセージを初期化するコールバック関数を渡します。

これで、サインアップ失敗時のハンドリングの実装は完了です。

動作確認

単体での動作確認がしづらいため、最後にまとめて確認します。

results matching ""

    No results matching ""