はじめに
現在、駆け出しエンジニア同士でチームを結成しアプリ開発をしています。
アプリ開発の過程でChakraUIとReact Hook Formの組み合わせ方法について学んだので、共有します。
React Hook Formの導入にきっかけ
Reactのフォームバリデーションライブラリとして有名なReact Hook Formですが、当初アプリ開発をスタートした際、この存在を知りませんでした。
そのため、各フォームの項目をuseStateで用意して独自にバリデーションを用意して進めていました。
中間レビューで現役エンジニアにレビューをもらった際に、これではパフォーマンスが悪いので、React Hook Formを導入したほうがいいと指摘があり、そこからReact Hook Form導入の検討を開始しました。
React Hook Form導入前は以下のような形で、これらが各入力に関わるコンポーネントに存在している状況でした。
また新規登録と編集で似たようなページがあるのですが、これもそれぞれコンポーネントを作り、実装してしまっており、共通化されていない状況でした。
イケてない。。。
export const PartnerInput = () => {
const [name, setName] = useState("");
const [zipcode, setZipcode] = useState("");
const [prefecture, setPrefecture] = useState("");
const [address, setAddress] = useState("");
const [phone, setPhone] = useState("");
const [memo, setMemo] = useState("");
React Hook Formを導入する
chakraUIとReact Hook Formを組み合わせて導入する方法は以下に記載があり、単一コンポーネントであれば、比較的簡単に導入できます。
chakraUIをインストール
yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
React Hook Formをインストール
yarn install react-hook-form
単一のコンポーネントで使う場合
フォームに必要なInputコンポーネントとSubmitするコンポーネントが同じ場合、
以下のchakraUIのリファレンスを参考に、進めると完成します。
import { useForm } from 'react-hook-form'
import {
FormErrorMessage,
FormLabel,
FormControl,
Input,
Button,
} from '@chakra-ui/react'
export const PartnerInput = () => {
const {
handleSubmit,
register,
formState: { errors, isSubmitting },
} = useForm()
const onSubmit = data => console.log(data);
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>
)
}
2つのコンポーネントの組み合わせで使う場合
一方で、新規登録と編集ページが同じである場合やデータA(例:注文)とデータB(例:顧客)の入力画面が同じである場合、コンポーネントを分けて、Inputコンポーネントを共通化したくなってくるかと思います。
その場合は少し記載方法が異なり、以下のReact Hook FormのuseFormContextに記載内容を参考に、進めると完成します。
import { useForm, FormProvider } from "react-hook-form";
export const Partner = () => {
const methods = useForm();
const onSubmit = data => console.log(data);
return (
<FormProvider {...methods} >
<form onSubmit={methods.handleSubmit(onSubmit)}>
<PartnerInput />
</form>
</FormProvider>
);
}
import { useFormContext } from "react-hook-form";
import { FormErrorMessage, FormLabel, FormControl, Input, Button } from "@chakra-ui/react";
export const PartnerInput = () => {
const {
register,
formState: { errors, isSubmitting },
} = useFormContext();
return (
<>
<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>
</>
);
};
3つのコンポーネントで組み合わせる場合
上記で進めると、親のコンポーネントと子のコンポーネントに分けることができるようになりますが、一方で、InputにまつわるFormControlやFormLabelなどをそれぞれのコンポーネントでChakraUIからimportしなくてはなりません。
ここでは、Text入力するためのInputコンポーネントを1つ作成し、顧客の新規登録、顧客の編集、注文の新規登録、注文の編集といった複数の利用シーンでコンポーネントを共通化できるようにします。
import { useFormContext } from "react-hook-form";
import { Button } from "@chakra-ui/react";
import { InputTextForm } from "../molecules/Input/Form/InputTextForm";
export const PartnerInput = () => {
const {
register,
formState: { errors, isSubmitting },
} = useFormContext();
return (
<>
<InputTextForm
id="name"
label="First name"
placeholder="name"
isInvalid={errors?.name}
register={register("name", {
required: "This is required",
minLength: { value: 4, message: "Minimum length should be 4" },
})}
/>
<Button mt={4} colorScheme="teal" isLoading={isSubmitting} type="submit">
Submit
</Button>
</>
);
};
import { FormLabel, FormControl, Input, Box, FormErrorMessage } from "@chakra-ui/react";
export const InputTextForm = (props) => {
const { register, id, label, isInvalid, placeholder } = props;
return (
<>
<FormControl id={id} isInvalid={isInvalid}>
<FormLabel htmlFor={id}>{label}</FormLabel>
<Box>
<Input {...register} type="text" placeholder={placeholder} />
<FormErrorMessage>{isInvalid && isInvalid.message}</FormErrorMessage>
</Box>
</FormControl>
</>
);
};
最後に
上記のように共通化することで、コードはすっきりするのですが、一方で、顧客の新規登録・編集で少し違った表現をしなければならなかったり、顧客と注文で違ったりすると、pathでの分岐やpropsの受け渡しがやたら多くなり可読性が落ちるなとも思いました。
この辺はuseStateでの実装を思いっきりリファクタリングにして(しすぎてみて)、やってみて理解が深まったところだったので、非常によい体験学習でした。
アプリリリースまであともう少し、引き続き、頑張っていこうと思います。