ChakraUIとReact-Hook-Formで活用フォームコンポーネントを作る
モチベーション
- ChakraUIでReact-Hook-Form(以下 RHF)を使う方法はいくつかあります
こちらは 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
...のように幾つも並べると可読性が大きく下がってしまいます
ということで、これを活用コンポーネントにしてしまいます
完成したコンポーネント
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>
);
}
);
使用例
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}
の様に渡せません。
なぜかはこちらの記事が参考になるかと思います
で、どうすればいいかというとforwordRef
を使っていい感じに渡してもらいます
export const ControlledInput = forwardRef<ControlledInputProps, 'input'>(
(
{
label,
errors,
name,
isRequired,
formControlProps,
formLabelProps,
formErrorMessageProps,
...rest
}: Omit<ControlledInputProps, 'ref'>,
ref
) => {
return (
...
この部分です
props が(props, ref)
という型で渡されるので、これを受け取ります。
あとはInput
にref={ref}
で渡せば OK です
コンポーネントの利用
うまくコンポーネントを作成できたので、普段使用するようにコンポーネントを使用できます。
<ControlledInput
label='ユーザー名'
errors={errors}
isRequired
{...register('name')}
/>
errors
とlabel
は独自のものですが、{...register('name')}
という記法はいつものものですね。
また、InputProps
中の属性type
を指定することでパスワードやメール、数値型も対応できます
<ControlledInput
label='パスワード'
errors={errors}
isRequired
type='password'
{...register('password')}
/>
それだけでなく、chakra のas
を活用するとテキストボックスを扱えます
<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 でフォームの値を変形できるのはめちゃくちゃいいですね
こちらの記事がとてもわかりやすいです
最後に
ここまでご覧いただきありがとうございます。
本記事はベストプラクティスでもなんでもないですが、chakra と RHF のコンポーネント化の記事が見当たらないので作成してみました。
間違いやもっとこうした方がいいよ!等ありましたら、コメントや Twitter の DM で教えていただけると嬉しいです!
Discussion
とても良い記事をありがとうございます!
一つ訂正の提案なのですが、
type ControlledInputProps
のPartial<FieldErrorsImpl<Record<string, unknown>>>
はPartial<FieldErrors<Record<string, unknown>>>
でないとエラーが出るようでした。参考程度に、私の環境は以下です。
お役に立てたら嬉しいです🙇