📑

React Hook Formで自作コンポーネントをバリデーションする

2023/11/06に公開

はじめに

備忘録です。
React Hook Form(以下RHF)を使えば、こんな感じで簡単にフォームのバリデーションを実装することができますよね。

※ ChakraUIを使ってます

<Input
  id="name"
  placeholder="name"
  {...register("name", {
    required: "required",
  })}
/>;

これを自作のコンポーネントで行う方法です。

TL;DR

Controllerを使います。

実際にやってみる

適当にこんな感じのコンポーネントを作ってみます。

type Inputs = {
  name: string;
};

export const TestForm = () => {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<Inputs>();

  const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <FormControl isInvalid={!!errors.name}>
        <FormLabel htmlFor="name">名前</FormLabel>
        <Input
          id="name"
          placeholder="name"
          {...register("name", {
            required: "required",
          })}
        />
        <FormErrorMessage>
          {errors.name && errors.name.message}
        </FormErrorMessage>
      </FormControl>
      <Button mt={4} colorScheme="teal" isLoading={isSubmitting} type="submit">
        送信
      </Button>
    </form>
  );
};

フォームが空のときはエラーで弾かれます。問題なさそうです。

次に、こんな感じでモーダルからチェックボックスを複数選択できるような自作コンポーネントの場合を考えてみます。

モーダルのコードはこんな感じ。

type HobbyModalProps = {
  isOpen: boolean;
  onClose: () => void;
};

const HobbyModal = (props: HobbyModalProps) => {
  const hobbies = ["野球", "サッカー", "バスケ"];
  const [checkedValues, setCheckedValues] = useState<string[]>([]);

  const handleChange = (values: string[]) => {
    setCheckedValues(values);
  };

  const handleSubmit = () => {
    console.log(checkedValues);
    props.onClose();
  };

  return (
    <Modal isOpen={props.isOpen} onClose={props.onClose}>
      <ModalOverlay />
      <ModalContent>
        <ModalHeader>選択</ModalHeader>
        <ModalCloseButton />
        <ModalBody>
          <CheckboxGroup
            onChange={(values) => handleChange(values as string[])}
          >
            <Stack>
              {hobbies.map((hobby) => (
                <Checkbox key={hobby} value={hobby}>
                  {hobby}
                </Checkbox>
              ))}
            </Stack>
          </CheckboxGroup>
        </ModalBody>

        <ModalFooter>
          <Button onClick={handleSubmit} colorScheme="blue" mr={3}>
            完了
          </Button>
          <Button onClick={props.onClose}>キャンセル</Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
};

まずcontrolをuseFormから取り出して、

const {
  register,
  handleSubmit,
  formState: { errors, isSubmitting },
  control,
} = useForm<Inputs>();

RHFのControllerで自作コンポーネントをラップします。
今回はonChangevalueだけを渡します。
※後述しますが、究極的にはonChangeだけあれば機能します。

<FormControl isInvalid={!!errors.hobby}>
  <FormLabel htmlFor="hobby">趣味</FormLabel>
  <Controller
    control={control}
    name="hobby"
    rules={{ required: "required" }}
    render={({ field: { onChange, value } }) => (
      <>
        <HobbyModal
          isOpen={isOpen}
          onClose={onClose}
          onChange={onChange}
          defaultValue={value}
        />
        <Button id="hobby" onClick={onOpen}>選ぶ</Button>
      </>
    )}
  />
  <FormErrorMessage>{errors.hobby && errors.hobby.message}</FormErrorMessage>
</FormControl>;

モーダルの方も併せて修正します。

type HobbyModalProps = {
  isOpen: boolean;
  onClose: () => void;
  onChange: (value: string[]) => void;
  defaultValue?: string[];
};

const HobbyModal = (props: HobbyModalProps) => {
  const hobbies = ["野球", "サッカー", "バスケ"];
  const [checkedValues, setCheckedValues] = useState<string[]>([]);

  const handleChange = (values: string[]) => {
    setCheckedValues(values);
  };

  const handleSubmit = () => {
    props.onChange(checkedValues);
    props.onClose();
  };

  return (
    <Modal isOpen={props.isOpen} onClose={props.onClose}>
      <ModalOverlay />
      <ModalContent>
        <ModalHeader>選択</ModalHeader>
        <ModalCloseButton />
        <ModalBody>
          <CheckboxGroup
            defaultValue={props.defaultValue}
            onChange={(values) => handleChange(values as string[])}
          >
            <Stack>
              {hobbies.map((hobby) => (
                <Checkbox key={hobby} value={hobby}>
                  {hobby}
                </Checkbox>
              ))}
            </Stack>
          </CheckboxGroup>
        </ModalBody>

        <ModalFooter>
          <Button onClick={handleSubmit} colorScheme="blue" mr={3}>
            完了
          </Button>
          <Button onClick={props.onClose}>キャンセル</Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
};

無事バリデーションが効くようになりました。

ちなみに

renderの引数であるfieldは何なのかというと、

  • OnChange: RHFに値を送る関数。一番重要。
  • onBlur: RHFのmodeがonBlurのときに使う...?(使ったことない…)
  • value: 現在RHFが管理している値の状態。
  • ref: エラーのときにそのフォームにフォーカスする時とかに使うDOMへの参照。
  • name: Controllerに渡すnameと同じ。コンポーネントによっては必要になる

なので、必要がなければonChangeだけ渡せばOK。

Discussion