IdoPesok / zsa

https://zsa.vercel.app
MIT License
436 stars 13 forks source link

Advanced React Hook Form Integration #104

Closed nahtnam closed 3 weeks ago

nahtnam commented 3 weeks ago

One thing I've been struggling with it all of the boilerplate code involved with setting up a form with server actions (not specific to this library)

One awesome idea that I think might work well would be if ZSA could automatically coerce the form errors and ZSA errors into react hook form errors.

What the integration would do is:

API wise, I'm not too sure what that would look like. Maybe something that looks like this:

const [form, ...otherStuff] = useReactServerActionHookForm(action, additionalReactHookFormProps)
nahtnam commented 3 weeks ago

Perhaps it would be fitting under zsa-react-hook-form

nahtnam commented 3 weeks ago

A very rudimentary idea of what I was thinking (without proper types or generics)

'use client';
import { useServerAction } from 'zsa-react';
import { useForm } from 'react-hook-form';
import { createServerAction, type inferServerActionInput } from 'zsa';
import { z } from 'zod';

const myAction = createServerAction()
  .input(z.object({ num: z.number() }))
  .handler(({ input }) => {
    return input.num + 1;
  });

type Input = inferServerActionInput<typeof myAction>;

export function useTest() {
  const serverAction = useServerAction(myAction);
  const { execute } = serverAction;
  const form = useForm<Input>({});

  async function handleSubmit(data: Input) {
    const [_data, err] = await execute(data as any); // should be as FormDataLikeInput
    if (!err) return;

    if (err.code === 'INPUT_PARSE_ERROR') {
      for (const fieldErrorKey of Object.keys(err.fieldErrors)) {
        const fieldError =
          err.fieldErrors[fieldErrorKey as keyof typeof err.fieldErrors];
        for (const error of fieldError ?? []) {
          form.setError(fieldErrorKey as any, { message: error });
        }
      }

      const formErrors = err.formErrors;
      if (formErrors.length) {
        form.setError('root.formErrors', { message: formErrors });
      }

      return;
    }

    form.setError('root.serverError', {
      ...err,
    });
  }

  return [serverAction, form, form.handleSubmit(handleSubmit)];
}
IdoPesok commented 3 weeks ago

Hi, thank you for bringing this up. Good points.

Attaching #70 to this -- the most recent messages deal with RHF x ZSA.

Here is a helpful code snippet from that thread:

export function rhfErrorsFromZsa<T extends FieldValues = FieldValues>(
  error: TZSAError<any> | null
): FieldErrors<T> | undefined {
  if (!error) return
  const {code: type, fieldErrors, formErrors, message} = error
  return {
    root: {type, message: message ?? formErrors?.[0]},
    ...Object.fromEntries(Object.entries(fieldErrors ?? {}).map(([name, errors]) => [name, {message: errors?.[0]}])),
  } as FieldErrors<T>
}

then on the client you should be able to do something like this:

const { error, data, isPending, execute } = useServerAction(myAction)
const form = useForm<Data>({
    mode: "onTouched",
    resolver: zodResolver(zData),
    errors: error ? rhfErrorsFromZsa(error) : undefined, // get rhf errors
    defaultValues: data ?? defaultData,
  })

However I understand that this is not ideal. Currently working on better error handling that should make this much smoother (see #102). Will ping here once its ready to go, should be soon! Thank you for posting these code snippets, will help me with my new PR + write docs to make it more clear. LMK if the above code doesn't work.

IdoPesok commented 3 weeks ago

Hi, with zsa@0.3.4 you can now customize errors how you like them. Please check out these docs for an example with react hook form. Appreciate any feedback!