ReactでSubmit Buttonを作成する(2.コンポーネント作成編)

こんにちは!

今回の記事は前回の記事である「ReactでSubmit Buttonを作成する(1.環境作成編)」の続きになります。

まだReactでWebアプリケーションを作成する準備が整っていない方で今回のコンポーネントを作成してみたい方は、こちらの記事から環境を作成されることをお読みください。

それでは早速始めていきましょう!

1.react-hook-formライブラリをインストールする

React19からの新機能である「useActionState」をRC版で使用しようと考えていたのですが、「hero icons」や「headless ui」などがまだ対応していないため、今回は「react-hook-form」を使用していきます。actionの状態を監視して、処理をされている間には送信している状態であるという変数の状態を変えることができる便利なhooksを提供しているReactのライブラリになりますので、今回はこれを使用します。(筆者はNext.jsを愛用しているので, useFormStateが使用できるReact19のバージョンを自動で必要パッケージを取得してくれます。正直React19の実験版を使用できるならそちらを利用したほうが記述が楽なのでおすすめです)

コマンドラインツールで下記のコマンドを入力します。

$ npm i react-hook-form

使用プロジェクト直下で必要パッケージを取得したら次に進みます。

2.formを作成する

App.tsxを下記のように書き換えます

import './App.css';
import { useEffect, useState } from 'react';
import SubmitButton from './components/SubmitButton/SubmitButton';
import { useForm } from 'react-hook-form';


type FormData = {
  username: string;
  email: string;
};

function App() {

  const { register, handleSubmit, formState: { errors, isSubmitting, isDirty, isValid } } = useForm<FormData>({
    mode: 'onChange',
  });

  const onSubmit = async (data: FormData) => {
    const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

    await wait(1000);
    console.log("フォームデータ", data);
  };

  return (
    <div className='flex justify-center w-full'>
      <div>
        <form onSubmit={handleSubmit(onSubmit)}>
          <div>
            <label>
              ユーザー名:
              <input type="text" {...register('username', { required: 'ユーザー名を入力してください' })} />
            </label>
            {errors.username && <p style={{ color: 'red' }}>{errors.username.message}</p>}
          </div>

          <div>
            <label>
              メールアドレス:
              <input
                type="email"
                {...register('email', { required: 'メールアドレスを入力してください', pattern: /^\S+@\S+$/i })}
              />
            </label>
            {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
          </div>

          <SubmitButton
            isPending={isSubmitting}
          >
            送信
          </SubmitButton>
        </form>

      </div>
    </div>
  );
}

export default App;

そうしたら「src」フォルダ直下に「components」フォルダを作成して、「SubmitButton」フォルダを作成し、「SubmitButton.tsx」ファイルを作成し下記の内容を書き込みます。

'use client'

import { ArrowPathIcon } from '@heroicons/react/16/solid'

export type SubmitButtonProps = {
  children: React.ReactNode
  isPending: boolean
}

export const SubmitButton = ({ children, isPending = false }: SubmitButtonProps) => {

  return (
    <>
      {isPending ? (
        <button
          type='submit'
          disabled={isPending}
          className='w-32 bg-blue-800 rounded-md text-white flex justify-center items-center p-2'
        >
          <ArrowPathIcon className='w-5 h-5 animate-spin' />送信中
        </button>
      ) : (
        <button
          type='submit'
          className='w-32 bg-blue-500 rounded-md text-white flex justify-center items-center p-2 hover:opacity-60 duration-200'
        >
          {children}
        </button>
      )}
    </>

  )
}

export default SubmitButton

上記のコンポーネントを作製したら、プロジェクトを起動します。

$ npm start

上記のコードでは、「App.tsx」内でonSubmit関数を作成し、useFormで作成したhandleSubmit関数のコールバックにonSubmit関数を渡すことで、onSubmit関数の非同期的な処理が完了するまで、isSubmittingの状態変数を「true」にしています。

今回はonSubmit関数内の処理を1秒待って送信するデータをコンソール上に表示するといった処理にすることで疑似的に通信のような挙動を作成しています。

また、useFormのカスタムHookを使用することで、ユーザー名とメールアドレスのバリデーションを付けることもできたので、特に送信ボタンとは関係ありませんが、一応実装しています。

今回は「SubmitButton」を作成するということで、こちらについて詳しく見ていこうと思います。PropsにはchildrenとisPendingを受け取ります。childrenには送信前のボタンのテキストを渡して描画することができます。そしてisPendingは、useFormでhandleSubmit関数に渡されたコールバック関数の処理が実行されている間isSubmittingをtrueにする処理を書いたのでisSubmittingをコンポーネントに反映させるためのpropsとして渡しています。isPendingがtureの時はボタンがクリックできないようになり専用のアニメーションが実行されます。逆に通常の状態であるisPendingがfalseの時はボタンを押してformのactionを実行可能な状態にしています。

isPending周りの処理がわからない方は「分割代入」と「三項演算子」を学習してみることで、書き方の何をやっているかを理解できるかもしれません。

4.最後に

ここまで「Submit Button」の作成記事を読んでいただきありがとうございます。React19からはuseFormStateというHooksがでるので、正式版がリリースされるか、Next.js関係の簡単な記事を書いたらまたそこら辺の紹介もしてみようと思います。