React Hook Formを活用して登録画面に入力フォームとバリデーションを設定する

2024/01/22に公開

はじめに

前回まではアカウント登録画面でカードコンポーネントを使ってタイトルやメッセージ、ソーシャルボタンなどを配置していきました。
最後に「入力フォーム」というテキストを一旦入れるところまで対応したかと思いますが、ここに実際の入力フォームを作っていきます。
ここでは主にReact Hook FormとZodという入力フォームに必要な機能を揃えたライブラリを活用していきますのでこのReact Hook FormとZodについても少し解説しています。

https://shinagawa-web.com/blogs/nextjs-tailwind-shadcn-account-registration-form

今回のゴール

この記事のゴールはアカウント登録画面で名前、メールアドレス、パスワードを入力。エンターキーを押すか、「アカウントを登録する」ボタンをクリックすると、入力内容をコンソールログで確認できる状態までです。
またメールアドレスの入力時にはメールアドレスとして正しい形式になっているかをチェックする仕組みを実装していますので間違った入力をした場合はエラーを表示するなどバリデーションとしての機能も実装していきます。

Image from Gyazo

入力内容は最終的にはDBに保存するのですが、そこまではまだ色々と実装する必要がありますので、まずは入力内容を取得できるところまでとします。

React Hook Formとは

Reactでフォームの状態管理とバリデーションを効率的に行うためのライブラリです。フォームの入力やバリデーションを簡単に扱うことができ、パフォーマンスにも優れています。主な特徴は以下の通りです:

  • パフォーマンスの最適化: フォームの入力やバリデーションの状態変更に伴う再レンダリングを最小限に抑える設計になっており、大規模なフォームでもスムーズに動作します。

  • 簡潔なAPI: フォームの状態管理やバリデーションを簡潔に扱えるAPIを提供しており、設定やコード量が少なくて済みます。

  • バリデーションのサポート: ネイティブのHTML5バリデーション、または外部ライブラリ(例:Zod)との統合が可能で、柔軟なバリデーションルールを設定できます。

  • フォームデータの管理: 入力値の取得やリセット、エラーメッセージの表示など、フォームデータに関する処理が簡単に行えます。

https://react-hook-form.com/

詳しい使い方については実装しながらご紹介します。

Zodとは

Zodは、TypeScriptのための型安全なスキーマバリデーションライブラリです。JavaScriptやTypeScriptのデータ構造を検証し、型の整合性を確認するために使われます。主な特徴は以下の通りです。

  • 型安全: ZodはTypeScriptと密接に統合されており、型エラーをコンパイル時に検出することができます。
  • 直感的なAPI: スキーマを定義するためのAPIが直感的で使いやすく、シンプルにバリデーションルールを作成できます。
  • 構造化されたエラーメッセージ: バリデーションエラーが構造化されており、どの部分が間違っているかを明確に把握できます。

例えばこんな感じで使えます。

  • name: 最低1文字以上
  • age: 数字かつ正の数

という条件で設定しており、入力した内容が正しいか間違っているかをチェックできます。
今回の場合は入力内容は条件に合致しているので「バリデーション成功」が返ってきます。

import { z } from 'zod';

const userSchema = z.object({
  name: z.string().min(1),
  age: z.number().int().positive(),
});

const result = userSchema.safeParse({ name: "Alice", age: 30 });

if (result.success) {
  console.log("バリデーション成功:", result.data);
} else {
  console.log("バリデーションエラー:", result.error.errors);
}

https://github.com/colinhacks/zod

Zodでスキーマの実装

早速実装に入っていきます。

まずはzodライブラリのインストールをします。
通常であれば下記のコマンドでインストールになります。

$ npm i zod

ですが、この後使用するshadcnのフォームコンポーネントのインストールで関連するライブラリとしてzodも一緒にインストールされますので、そちらを実行します。

$ npx shadcn@latest add form

package.jsonを確認すると幾つかのライブラリが入っていることが確認できます。

package.json
{
  "name": "tutorial-nextjs-14-auth",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint --fix && eslint --fix '**/*.{js,ts,tsx}' && prettier --write '**/*.{ts,tsx,scss}'"
  },
  "dependencies": {
+  "@hookform/resolvers": "^3.9.0",
    "@radix-ui/react-icons": "^1.3.0",
+   "@radix-ui/react-label": "^2.1.0",
    "@radix-ui/react-slot": "^1.1.0",
    "class-variance-authority": "^0.7.0",
    "clsx": "^2.1.1",
    "lucide-react": "^0.438.0",
    "next": "14.0.4",
    "react": "^18",
    "react-dom": "^18",
+   "react-hook-form": "^7.53.0",
    "react-icons": "^5.3.0",
    "tailwind-merge": "^2.5.2",
    "tailwindcss-animate": "^1.0.7",
+   "zod": "^3.23.8"
  },
  // 一部抜粋
}

shadcnのフォームコンポーネントではReact Hook FormやZodを使うところまでをセットで考慮されているため、このようにまとめてインストールされます。

ということでZodが入りました。

次にスキーマを定義します。

今回は名前、メールアドレス、パスワードを入力するのでそれらのスキーマとして定義します。

/schemas配下にindex.tsファイルを作成しコードを書いていきます。

index.ts
import * as z from 'zod'

export const RegisterSchema = z.object({
  email: z.string().email({
    message: 'メールアドレスを入力してください',
  }),
  password: z.string().min(6, {
    message: 'パスワードは6文字以上で入力してください',
  }),
  name: z.string().min(1, {
    message: '名前を入力してください',
  }),
})

名前、メールアドレス、パスワードの入力条件を設定しました。合わせて入力条件に合致していないときに画面に表示するメッセージもこちらで定義しました。
今回は最低限のバリデーションを設定しましたが、最低入力文字数だけでなく最大で何文字まで入力を許可するか?など様々な設定が可能となっています。
具体的にどのようなチェックが可能かはこちらが参考になるかと思います。

https://github.com/colinhacks/zod?tab=readme-ov-file#strings

ここからは補足です。

私の環境ではフォームコンポーネントでESLintのエラーが出ました。

Image from Gyazo

ControllerProps,FieldPath,FieldValuesこれらが見つからないというようなメッセージになります。しかし、実際は存在しています。こちらは型定義になるので私の環境では下記の設定をしてエラーを回避しました。もしESLintのメッセージが出ていたら設定変更してみてください。

form.tsx
- import {
-   Controller,
-   ControllerProps,
-   FieldPath,
-   FieldValues,
-   FormProvider,
-   useFormContext,
- } from "react-hook-form";
+ import type { FieldPath, FieldValues, ControllerProps } from 'react-hook-form'
+ import { Controller, FormProvider, useFormContext } from 'react-hook-form'

入力コンポーネントの実装

入力コンポーネントにはフォームコンポーネント以外にinputコンポーネントが必要となりますので、こちらをインストールします。

npx shadcn@latest add input

register-form.tsxを開いて下記のコードを貼り付けてください。

register-form.tsx
'use client'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import * as z from 'zod'
import { RegisterSchema } from '@/schema'
import { Button } from '../ui/button'
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '../ui/form'
import { Input } from '../ui/input'
import { CardWrapper } from './card-wrapper'

export const RegisterForm = () => {
  const form = useForm<z.infer<typeof RegisterSchema>>({
    resolver: zodResolver(RegisterSchema),
    defaultValues: {
      email: '',
      password: '',
      name: '',
    },
  })
  const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
    console.log(values)
  };
  return (
    <CardWrapper
      headerLabel="各項目を入力してアカウントを作成"
      buttonLabel="既にアカウントを登録済みの方はコチラ"
      buttonHref="/auth/login"
      showSocial
    >
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <div className="space-y-4">
            <FormField
              control={form.control}
              name="name"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>名前</FormLabel>
                  <FormControl>
                    <Input
                      {...field}
                      placeholder="ネクスト ジェイエス"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="email"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>メールアドレス</FormLabel>
                  <FormControl>
                    <Input
                      {...field}
                      placeholder="nextjs@example.com"
                      type="email"
                    />
                  </FormControl>
                  <FormMessage />
                </FormItem>
              )}
            />
            <FormField
              control={form.control}
              name="password"
              render={({ field }) => (
                <FormItem>
                  <FormLabel>パスワード</FormLabel>
                  <FormDescription>
                    パスワードは6文字以上で設定してください
                  </FormDescription>
                  <FormControl>
                    <Input {...field} placeholder="******" type="password" />
                  </FormControl>

                  <FormMessage />
                </FormItem>
              )}
            />
          </div>
          <Button type="submit" className="w-full">
            アカウントを作成する
          </Button>
        </form>
      </Form>
    </CardWrapper>
  )
}

少しずつ分割して実装内容の補足をしていきます。

const onSubmit = (values: z.infer<typeof RegisterSchema>) => {
  console.log(values)
};

(※一部省略)

<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">

フォーム全体を囲うものとしてFormコンポーネントとformタグが登場します。
FormコンポーネントではuseFormで定義した内容を渡すことで、以降のFormFieldコンポーネントで使用できるようになります。

Image from Gyazo

formタグでは最終的に入力内容を確定したときにエンターキーを押下、もしくは「アカウントを作成する」ボタンを押下した際のイベントを登録します。
今回はonSubmitで引数で渡された内容を表示するよう実装しています。
こちらは追々、データベースに連携できるよう修正していく予定です。

<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>メールアドレス</FormLabel>
      <FormControl>
        <Input
          {...field}
          placeholder="nextjs@example.com"
          type="email"
        />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

次に各入力部分に関する補足です。
こちらはアカウント登録時のメールアドレスを入力する箇所になります。
formの内容をFormFieldに渡すことで/schemas/index.tsファイルで設定した内容が反映されます。

動作確認

それでは最後に実際に動かしてみて想定通りの挙動になっているか確認します。
アカウント登録画面を開き(http://localhost:3000/auth/register)
まずは何も入力せずにエンターキーを押します。
すると入力されていないことを知らせるメッセージが表示されます。
その後、名前、メールアドレス、パスワードを入力してエンターを押すと、
右側にコンソールログが表示され、入力された内容が確認できます。
onSubmitで入力内容を受け取れたのでこれらを活用しデータベースに登録する流れとなります。

Image from Gyazo

さいごに

今回はReact Hook FormやZodを活用し名前やメールアドレスの入力フォームを作成しました。
また入力が正しく行われているかをチェックするバリデーション処理も実装しました。
次回はユーザーが入力した内容をもとにデータベースに登録していく機能を実装していきます。

次の記事はこちら

https://shinagawa-web.com/blogs/nextjs-server-actions-form-data

記事に関するお問い合わせ📝

記事の内容に関するご質問、ご意見などは、下記よりお気軽にお問い合わせください。
ご質問フォームへ

技術支援などお仕事に関するお問い合わせ📄

技術支援やお仕事のご依頼に関するお問い合わせは、下記よりお気軽にお問い合わせください。
お問い合わせフォームへ