reactjsreact-hook-formzod

react-hook-form field array custom refine error state won't update


My fieldValues ​​are as follows:

export const schema = z.object({
  urls: z
    .array(
      z.object({
        path: z.string().url(),
        main: z.boolean(),
        id: z.string(),
      })
    )
    .min(1)
    .max(3)
    .refine(
      (urls) => {
        const result = urls.some((url) => url.main);
        console.log("refine", result);
        return result;
      },
      {
        message: "at least there is a main site!",
      }
    ),
});

There is a custom validation logic, the logic is that urls must have a url marked as the main site.

But I found that the built-in min() and max() work well, and although my custom validation rules are triggered, error.message is not updated.

Form.tsx

import { Controller, useFieldArray, useFormContext } from "react-hook-form";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import RemoveCircleTwoToneIcon from "@mui/icons-material/RemoveCircleTwoTone";
import { Button } from "@mui/material";
import RHFTextField from "./RHFTextField";
import RHFSwitch from "./RHFSwitch";

const Form = () => {
  const { control } = useFormContext<Schema>();

  const { append, fields, remove } = useFieldArray({
    control,
    name: "urls",
  });

  return (
    <Controller
      name="urls"
      control={control}
      render={({ field, fieldState: { error } }) => {
        return (
          <Stack spacing={1}>
            <Stack direction={"row"} alignItems={"center"} spacing={1}>
              <Typography variant={"h6"}>urls</Typography>
              <Typography variant={"body2"} color={"error"}>
                {error?.message}
              </Typography>
            </Stack>
            {fields.map((url, index) => (
              <Stack key={url.id} direction={"row"}>
                <div className="pr-1 h-[38px] flex items-center">
                  <RemoveCircleTwoToneIcon
                    color={"error"}
                    onClick={() => {
                      remove(index);
                    }}
                  />
                </div>
                <RHFTextField name={`urls.${index}.path`} className="flex-1" />
                <RHFSwitch name={`urls.${index}.main`} />
              </Stack>
            ))}
            <Button
              variant="text"
              onClick={() => {
                append({
                  path: "",
                  main: false,
                  id: window.crypto.randomUUID(),
                });
              }}
            >
              add url
            </Button>
          </Stack>
        );
      }}
    />
  );
};

export default Form;

full code

https://codesandbox.io/p/sandbox/react-hook-form-1-wwqnkk?file=%2Fsrc%2Fschema.ts

How to test?

  1. click add url button.

enter image description here

  1. click the Switch button, At this time, the console prints refine true, indicating that the verification passed, but error?.message not updated.

enter image description here


Solution

  • You can use trigger method to trigger "urls" path validation on RHFSwitch component changes:

    const RHFSwitch = memo(({ label, name, ...props }: Props) => {
      const { control, trigger } = useFormContext();
    
      return (
        <Controller
          name={name}
          control={control}
          render={({
            field: { value, onChange, ...field },
            fieldState: { error },
          }) => (
            <Stack direction={"row"} justifyContent={"space-between"}>
              <Typography variant="h6">{label}</Typography>
              <Switch
                checked={value}
                onChange={(e) => {
                  onChange(e);
                  trigger("urls");
                }}
                {...field}
                {...props}
              />
            </Stack>
          )}
        />
      );
    });
    

    Explanation

    The errors handled by zod's refine method are registered in the "urls" path of the errors object, which you can find inside formState.

    The "urls" path validations are only triggered when you add/remove an item to the fields array.

    I'm not sure, but I guess RHF does this on purpose, to avoid unneeded re-rendering of stuff.