2

I have the following schema in my RHF form:

const latexSchema = z.string().refine(
  () => {
    return solutionRef.current?.editor?.state.doc.textContent.length! >= 10;
  },
  'Wymagane jest co najmniej 10 znaków.'
);
const imagesSchema = files.min(1, 'Wymagany jest co najmniej jeden plik.');
const videoSchema = file;

const currentUserQuery = useCurrentUser();
const user = currentUserQuery.data!;

const schema = useMemo(
  () => saveHomeworkSchema.extend({
    description: saveHomeworkSchema.shape.description.refine(
      () => descriptionRef.current?.editor?.state.doc.textContent.length! >= 10,
      'Wymagane jest co najmniej 10 znaków.'
    ),
    solution: solution
      .refine(
        (solution) => {
          return (
            latexSchema.safeParse(solution?.latex).success ||
            imagesSchema.safeParse(solution?.images).success ||
            videoSchema.safeParse(solution?.video).success ||
            solutionSteps.min(1).safeParse(solution?.steps).success
          );
        },
        {
          path: [''],
          message: 'Musisz podać przynajmniej jeden format rozwiązania.',
        }
      )
      .refine(
        (solution) =>
          !solution?.video?.uploading ||
          solution?.images?.some((el) => el.uploading),
        {
          path: [''],
          message: 'Trwa przesyłanie pliku...',
        }
      ),
    }),
  []
);

So I want to add an error to solution field when none of solution.latex, solution.images etc. are valid. The problem is that even though the refine is running, the errors.solution is not updated... I have mode:all on my form and resolver: zodResolver(schema). As a workaround I added:

useEffect(() => {
  const callback = subscribe({
    formState: {
      values: true,
    },
    callback: ({ name }) => {
      if (name?.startsWith('solution')) {
        trigger('solution');
        trigger(name as FieldPath<HomeworkSchemaType>);
      }
    },
  });

  return () => callback();
}, [subscribe]);

But I don't think its a good solution. Any ideas why refine does not updated errors in that case? I guess this is beacuse my initial solution schema is

export const solutionSteps = z.array(solutionStep);

export const solution = z.object({
  latex: z.string().nullable(),
  images: z.array(file),
  video: file.nullable(),
  files: z.array(z.string()),
  steps: solutionSteps,
});

so it's valid by default but I expected that refine will help here.

1
  • If you add a console.log, is the condition in the refine failing/succeeding when you expect it? Commented Nov 14 at 16:58

1 Answer 1

1

When you type in solution.latex:

  1. RHF calls your resolver with names = ['solution.latex'].

  2. zodResolver(schema) runs validation for the whole schema (including your solution.refine(...)).

  3. Your refine on solution correctly returns a Zod issue whose path is something like:

    path: [''] // relative to the "solution" object
    
    
  4. After validation, zodResolver filters the issues down to just the field(s) being validated.
    Since this run was for solution.latex, it only keeps issues whose path maps to solution.latex (or deeper), and it drops the parent-level solution issue.

  5. Result: errors.solution doesn’t change when only solution.latex is being validated.

This behavior is intentional: RHF focuses on field-level validation. The maintainer explicitly says that for dependent / cross-field validation you need to use trigger to revalidate the relevant fields.

Your workaround:

if (name?.startsWith('solution')) {
  trigger('solution');
  trigger(name as FieldPath<HomeworkSchemaType>);
}

is basically doing exactly what the maintainer recommends:

“React hook form focus on field level validation, if you need dependent update use trigger api.”

So it’s not a hack, it’s the pattern.

So how do you do this cleanly?

You have a few options. All of them boil down to “when child fields change, also validate the parent”.

1. Use useWatch + trigger for solution

This is the same logic as your subscribe, but a bit more idiomatic:

const { control, trigger } = useForm<HomeworkSchemaType>({
  resolver: zodResolver(schema),
  mode: 'all',
});

const solutionValue = useWatch({
  control,
  name: 'solution',
});

useEffect(() => {
  // Whenever any solution.* field changes, revalidate the whole solution object
  void trigger('solution');
}, [solutionValue, trigger]);

Now your Zod refine on solution will run and its error will actually be attached to solution (because this time RHF asked to validate the solution field itself).

Your schema can stay essentially as-is (I’d just drop the path: [''], see below):

const schema = useMemo(
  () =>
    saveHomeworkSchema.extend({
      solution: solution
        .refine(
          (solution) =>
            latexSchema.safeParse(solution?.latex).success ||
            imagesSchema.safeParse(solution?.images).success ||
            videoSchema.safeParse(solution?.video).success ||
            solutionSteps.min(1).safeParse(solution?.steps).success,
          {
            // Let Zod use the default path => attaches to "solution"
            message: 'Musisz podać przynajmniej jeden format rozwiązania.',
          }
        )
        .refine(
          (solution) =>
            !solution?.video?.uploading ||
            solution?.images?.some((el) => el.uploading),
          {
            message: 'Trwa przesyłanie pliku...',
          }
        ),
    }),
  []
);

Zod will produce issues for solution itself, and trigger('solution') will let RHF keep those.

2. Trigger in field onChange

If you want more fine-grained control, you can trigger in the child fields themselves:

<Controller
  control={control}
  name="solution.latex"
  render={({ field }) => (
    <MyLatexEditor
      value={field.value}
      onChange={(value) => {
        field.onChange(value);
        void trigger('solution'); // revalidate combined rule
      }}
    />
  )}
/>

You’d do the same for solution.images, solution.video, etc.

Why mode: 'all' doesn’t help

mode: 'all' only controls when validation happens (onChange, onBlur, onSubmit).
It does not change what RHF asks the resolver to validate.

Even with mode: 'all', when you type in solution.latex, RHF still calls the resolver as if it’s validating only 'solution.latex', and it still filters out the solution-level issues.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.