🐙

ChakraUIとReact-Hook-Formで活用フォームコンポーネントを作る

2022/11/23に公開約7,500字

モチベーション

https://chakra-ui.com/getting-started/with-hook-form

こちらはchakra公式が紹介しているRHFの使用法です

import { useForm } from 'react-hook-form'
import {
  FormErrorMessage,
  FormLabel,
  FormControl,
  Input,
  Button,
} from '@chakra-ui/react'

export default function HookForm() {
  const {
    handleSubmit,
    register,
    formState: { errors, isSubmitting },
  } = useForm()

  function onSubmit(values) {
    return new Promise((resolve) => {
      setTimeout(() => {
        alert(JSON.stringify(values, null, 2))
        resolve()
      }, 3000)
    })
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormControl isInvalid={errors.name}>
        <FormLabel htmlFor='name'>First name</FormLabel>
        <Input
          id='name'
          placeholder='name'
          {...register('name', {
            required: 'This is required',
            minLength: { value: 4, message: 'Minimum length should be 4' },
          })}
        />
        <FormErrorMessage>
          {errors.name && errors.name.message}
        </FormErrorMessage>
      </FormControl>
      <Button mt={4} colorScheme='teal' isLoading={isSubmitting} type='submit'>
        Submit
      </Button>
    </form>
  )
}

ラベルと入力欄の部分はここになります

抜粋
      <FormControl isInvalid={errors.name}>
        <FormLabel htmlFor='name'>First name</FormLabel>
        <Input
          id='name'
          placeholder='name'
          {...register('name', {
            required: 'This is required',
            minLength: { value: 4, message: 'Minimum length should be 4' },
          })}
        />
        <FormErrorMessage>
          {errors.name && errors.name.message}
        </FormErrorMessage>
      </FormControl>

これをname, email, password...のように幾つも並べると可読性が大きく下がってしまいます

ということで、これを活用コンポーネントにしてしまいます

完成したコンポーネント

components/Input/ContorolledInput.tsx
import {
  FormControl,
  FormLabel,
  Input,
  FormErrorMessage,
  InputProps,
  forwardRef,
  FormControlProps,
  FormLabelProps,
  FormErrorMessageProps,
} from '@chakra-ui/react';
import { FieldErrorsImpl, Ref } from 'react-hook-form';

export type ControlledInputProps = {
  label: string;
  errors: Partial<FieldErrorsImpl<Record<string, unknown>>>;
  name: string;
  ref: Ref;
  isRequired?: boolean;
  formControlProps?: Omit<FormControlProps, 'isInvalid' | 'isRequired'>;
  formLabelProps?: FormLabelProps;
  formErrorMessageProps?: FormErrorMessageProps;
} & Omit<InputProps, 'isRequired'>;

export const ControlledInput = forwardRef<ControlledInputProps, 'input'>(
  (
    {
      label,
      errors,
      name,
      isRequired,
      formControlProps,
      formLabelProps,
      formErrorMessageProps,
      ...rest
    }: Omit<ControlledInputProps, 'ref'>,
    ref
  ) => {
    return (
      <FormControl
        isInvalid={Boolean(errors[name])}
        isRequired={isRequired}
        {...formControlProps}
      >
        <FormLabel {...formLabelProps}>{label}</FormLabel>
        <Input name={name} {...rest} ref={ref} />
        <FormErrorMessage {...formErrorMessageProps}>
          {errors[name]?.message}
        </FormErrorMessage>
      </FormControl>
    );
  }
);

使用例

nextjsで書きました
import { Button, Container, Textarea } from '@chakra-ui/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { NextPage } from 'next';
import { SubmitHandler, useForm } from 'react-hook-form';
import { z } from 'zod';
import { ControlledInput } from '../components/Input/ControlledInput';

export const schema = z.object({
  name: z.string().min(3, '名前は3文字以上で入力してください'),
  email: z.string().email('メールアドレスの形式が正しくありません'),
  password: z.string().min(8, 'パスワードは8文字以上で入力してください'),
  description: z
    .string()
    .max(100, '自己紹介は100文字以内にしてください')
    .optional(),
});
export type FormValues = z.infer<typeof schema>;
const Page: NextPage = () => {
  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });
  const onSubmit: SubmitHandler<FormValues> = (form) => {
    console.log(form);
    reset();
  };
  return (
    <Container as='form' p='10' boxShadow='md' borderRadius='md'>
      <ControlledInput
        label='ユーザー名'
        errors={errors}
        isRequired
        {...register('name')}
      />
      <ControlledInput
        label='メールアドレス'
        type='email'
        errors={errors}
        isRequired
        {...register('email')}
      />
      <ControlledInput
        label='パスワード'
        errors={errors}
        isRequired
        type='password'
        {...register('password')}
      />
      <ControlledInput
        label='自己紹介'
        errors={errors}
        as={Textarea}
        {...register('description')}
      />
      <Button onClick={handleSubmit(onSubmit)}>送信</Button>
    </Container>
  );
};

export default Page;

出力

解説

コンポーネントについて

refをどうやって渡すか

RHFのregister

(name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })

の型になっています

ここにrefがあるのですが、そのままpropsでref={ref}の様に渡せません。

https://zenn.dev/terrierscript/scraps/15ca11388f7424

なぜかはこちらの記事が参考になるかと思います

で、どうすればいいかというとforwordRefを使っていい感じに渡してもらいます

export const ControlledInput = forwardRef<ControlledInputProps, 'input'>(
  (
    {
      label,
      errors,
      name,
      isRequired,
      formControlProps,
      formLabelProps,
      formErrorMessageProps,
      ...rest
    }: Omit<ControlledInputProps, 'ref'>,
    ref
  ) => {
    return (
    ...

この部分です
propsが(props, ref)という型で渡されるので、これを受け取ります。
あとはInputref={ref}で渡せばOKです

コンポーネントの利用

うまくコンポーネントを作成できたので、普段使用するようにコンポーネントを使用できます。

<ControlledInput
  label='ユーザー名'
  errors={errors}
  isRequired
  {...register('name')}
/>

errorslabelは独自のものですが、{...register('name')}という記法はいつものものですね。

また、InputProps中の属性typeを指定することでパスワードやメール、数値型も対応できます

type='password'
<ControlledInput
  label='パスワード'
  errors={errors}
  isRequired
  type='password'
  {...register('password')}
/>

それだけでなく、chakraのasを活用するとテキストボックスを扱えます

as={Textarea}
<ControlledInput
  label='自己紹介'
  errors={errors}
  as={Textarea}
  {...register('description')}
/>

かなり活用的なコンポーネントになりましたね。あとはデザインを凝るなどすればそれなりに使えるものになると思います

zodの利用

validationとしてzodを使用しました

export const schema = z.object({
  name: z.string().min(3, '名前は3文字以上で入力してください'),
  email: z.string().email('メールアドレスの形式が正しくありません'),
  password: z.string().min(8, 'パスワードは8文字以上で入力してください'),
  description: z
    .string()
    .max(100, '自己紹介は100文字以内にしてください')
    .optional(),
});

...

  const {
    register,
    handleSubmit,
    reset,
    formState: { errors },
  } = useForm<FormValues>({
    resolver: zodResolver(schema),
  });
  
  ...

zodを使用することにより、validationをjsxの箇所ではなく、外部で記述することができます
これにより、コンポーネントの構成がわかりやすくなります。

もちろん、バリデーションライブラリとしてもさまざまな機能が用意されているので、非常に使い勝手がいいです。
特にtransformでフォームの値を変形できるのはめちゃくちゃいいですね

https://zenn.dev/uttk/articles/bd264fa884e026#transformer-について

こちらの記事がとてもわかりやすいです

最後に

ここまでご覧いただきありがとうございます。

本記事はベストプラクティスでもなんでもないですが、chakraとRHFのコンポーネント化の記事が見当たらないので作成してみました。

間違いやもっとこうした方がいいよ!等ありましたら、コメントやTwitterのDMで教えていただけると嬉しいです!

Discussion

ログインするとコメントできます