[React Hook Form] useFormContext を使用して、フォーム要素をコンポーネント化する

# 概要

この記事では、**React Hook Form** の `useFormContext` フックを使用して、フォーム要素をモジュラーでメンテナンスしやすいコンポーネントに分割する方法を詳しく解説します。

# useFormContext とは

`useFormContext` は **React Hook Form** の `API` で、フォームの状態と機能をネストされたコンポーネント間で共有するための強力な機能です。

## 特徴

- フォームの状態をグローバルに管理
- 子コンポーネントから簡単にフォームメソッドにアクセス
- 再利用可能で柔軟なフォームコンポーネントの作成を可能に

# 実装例

今回は以下のコンポーネントを実装していきます。

1. `UseFormContext.tsx`（親となるフォームコンポーネント）
1. `FormInput.tsx`（インプット）
1. `FormDropDown.tsx`（ドロップダウン）

```tsx:
'use client'

import { useForm, SubmitHandler, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from 'zod'
import { FormInput } from "./FormInput";
import { FormDropdown } from "./FormDropDown";

const userSchema = z.object({
  name: z.string().min(1, '名前を入力してください'),
  category: z.string().min(1, 'カテゴリーを選択してください')
})

type UserData = z.infer<typeof userSchema>

export const TestUseFormContextComponent: React.FC = () => {
  const methods = useForm<UserData>({
    resolver: zodResolver(userSchema),
    mode: 'onChange'
  });

  const onSubmit: SubmitHandler<UserData> = data => { console.log(data) }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100 p-4">
      <FormProvider {...methods}>
        <form
          onSubmit={methods.handleSubmit(onSubmit)}
          className="bg-white shadow-md rounded-lg p-8 w-full max-w-md space-y-6"
        >
          <h2 className="text-2xl font-bold text-center text-gray-800 mb-4">ユーザー登録</h2>

          <div className="space-y-4">
            <FormInput
              name="name"
              label="名前"
            />

            <FormDropdown
              name="category"
              label="カテゴリー"
              options={[
                { value: "category1", label: "カテゴリー1" },
                { value: "category2", label: "カテゴリー2" },
                { value: "category3", label: "カテゴリー3" }
              ]}
            />

            <button
              type="submit"
              className="w-full bg-blue-600 text-white py-2 rounded-md hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
            >
              送信
            </button>
          </div>
        </form>
      </FormProvider>
    </div>
  )
}
```

```tsx:FormInput.tsx
'use client'

import { useFormContext } from 'react-hook-form'
import { useState } from 'react'
import { FormInputProps } from './types'

export const FormInput = ({
  name,
  label,
  placeholder,
  type = 'text',
  required = false,
}: FormInputProps) => {
  const {
    register,
    formState: { errors },
  } = useFormContext()
  const [isFocused, setIsFocused] = useState(false)

  const errorMessage = errors[name]?.message as string | undefined
  const { onBlur: registerOnBlur, ...registerRest } = register(name)

  return (
    <div className="space-y-1">
      <label
        htmlFor={name}
        className="block text-sm font-medium text-gray-700"
      >
        {label}
        {required && <span className="text-red-500 ml-1">*</span>}
      </label>
      <input
        id={name}
        type={type}
        placeholder={placeholder}
        className={`block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none sm:text-sm
          ${
            errorMessage
              ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
              : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
          }
          ${isFocused ? 'ring-2 ring-blue-500' : ''}
        `}
        onFocus={() => setIsFocused(true)}
        onBlur={(e) => {
          setIsFocused(false)
          registerOnBlur(e)
        }}
        {...registerRest}
      />
      {errorMessage && (
        <p className="mt-1 text-sm text-red-600">{errorMessage}</p>
      )}
    </div>
  )
}

```

```tsx:FormDropDown.tsx
'use client'

import { useFormContext } from 'react-hook-form'
import { FormDropdownProps } from './types'

export const FormDropdown = ({
  name,
  label,
  options,
  required = false,
  placeholder = '選択してください',
}: FormDropdownProps) => {
  const {
    register,
    formState: { errors },
  } = useFormContext()

  const errorMessage = errors[name]?.message as string | undefined

  return (
    <div className="space-y-1">
      <label
        htmlFor={name}
        className="block text-sm font-medium text-gray-700"
      >
        {label}
        {required && <span className="text-red-500 ml-1">*</span>}
      </label>
      <select
        id={name}
        {...register(name)}
        className={`block w-full px-3 py-2 border rounded-md shadow-sm focus:outline-none sm:text-sm
          ${
            errorMessage
              ? 'border-red-300 focus:ring-red-500 focus:border-red-500'
              : 'border-gray-300 focus:ring-blue-500 focus:border-blue-500'
          }
        `}
      >
        <option value="">{placeholder}</option>
        {options.map((option) => (
          <option key={option.value} value={option.value}>
            {option.label}
          </option>
        ))}
      </select>
      {errorMessage && (
        <p className="mt-1 text-sm text-red-600">{errorMessage}</p>
      )}
    </div>
  )
}
```

# まとめ
このようにすることで、コンポーネントを分割して作成することができるので便利ですね！