#Help me understand actionData's type serialization to reuse react-hook-form returned errors

1 messages · Page 1 of 1 (latest)

obsidian tide
#

Hey all,

First of all thanks for the cool framework, I think this is the future of web development.

I've been working on using react-hook-form for validating forms and I wanted to validate both at the client side (which is done automatically by the library) and the server side. I am doing the validation myself on the server side.

My doubt is that when returning a type in the action, the Route.ComponentProps actionData has that type but "expanded" where the original types are stripped but every field from the original type is there.

More context in the thread.

#

Here's the code:

The action:

export async function action({ request }: Route.ActionArgs) {
  const formData = await request.formData();

  const sch = formSchema.refine(
    async ({ email, password }) => {
      const user = await userRepository.byEmail(email);
      if (!user) {
        logger.info("login: user not found on the database");
        return false;
      }

      const validPassword = await passwordHasher.verify(
        user.passwordHash,
        password,
      );
      if (!validPassword) {
        logger.info("login: failed password verification");
        return false;
      }

      return true;
    },
    {
      path: ["root.server"],
      message: "Invalid email or password.",
    },
  );
  const resolver = zodResolver(sch);

  const { errors, data } = await validateFormData(formData, resolver);
  if (errors) {
    return {
      errors: errors,
    };
  }

  const redirectTo = safeRedirect(data.redirectTo, "/");

  const user = await userRepository.byEmail(data.email);
  if (!user) {
    throw new Error("login: could not find the user by email after validation");
  }

  return await authSession.createUserSession({ userId: user.id, redirectTo });
}
#

and the rest of the stuff

export const validateFormData = async <T extends FieldValues>(
  data: FormData,
  resolver: Resolver<T>,
): Promise<
  | {
      errors: FieldErrors<T>;
      data: undefined;
    }
  | {
      errors: undefined;
      data: T;
    }
> => {
  const formEntries = Object.fromEntries(data) as T;
  const { errors, values } = await resolver(
    formEntries,
    {},
    { shouldUseNativeValidation: false, fields: {} },
  );

  if (Object.keys(errors).length > 0) {
    return { errors: errors as FieldErrors<T>, data: undefined };
  }

  return { errors: undefined, data: values as T };
};
export default function LoginPage({ actionData }: Route.ComponentProps) {
  const [searchParams] = useSearchParams();
  const redirectTo = searchParams.get("redirectTo") || "/";
  const navigation = useNavigation();
  const form = useForm({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: "",
      password: "",
      redirectTo: "",
    },
    errors: actionData?.errors as FieldErrors<LoginFormSchema> | undefined, // -> this is the part where I have to use as because the type is lost
  });
  const submit = useSubmit();

  return ( /* component */ )
}

So what I understand is that the type is similar and not exactly the same. Am I correct?

This is the part that makes me believe this:

type ServerData<T> = T extends Response ? never : T extends DataWithResponseInit<infer U> ? Serialize<U> : Serialize<T>;

Where Serialize does the stripping, am I right?