[]previous and next page

React Hook Form to @shadow-js/forms

React Hook Form is React-only, so it has no port; @shadow-js/forms covers the same ground (a schema resolver, field arrays, server errors, focus on the first invalid…

For agentsUpdated

React Hook Form is React-only, so it has no port; @shadow-js/forms covers the same ground (a schema resolver, field arrays, server errors, focus on the first invalid field) with fields that are read by calling them. The Zod schema you pass to zodResolver goes to useForm({ resolver: standardSchema(schema) }) unchanged (standardSchema is from @shadow-js/forms/standard-schema), and any other Standard Schema library works the same way. The guide Forms covers the whole package; this is the mapping for a port.

React Hook Form @shadow-js/forms
useForm({ resolver: zodResolver(schema), defaultValues }) useForm({ resolver: standardSchema(schema), defaultValues }); the form holds the schema's input type, onSubmit receives its output
register("email") <input {...form.field("email").props()} />
formState.errors.email?.message form.field("email").error()
watch("email") form.watch("email") (an accessor) or field.value()
useFieldArray({ name }), fields, append, remove form.fields("tags"): append, remove, move, or <FieldArray>, with features: [fieldArrays] in the form; a row keeps its state when the rows around it move
handleSubmit(onValid) the onSubmit option; <Form form={form}> submits through form.handleSubmit
setError("root.server", ...) form.setError("", ...) or applyServerErrors(form, error)
reset(values) form.reset(values)

Two differences change how a ported component reads. There is no re-render: reading email.error() in JSX is the subscription, so nothing needs memo or useWatch. And validateOn defaults to "touched" (a problem appears when a field is left, then follows every keystroke), not React Hook Form's submit-first mode. The next component keeps a tag list and shows the old and the new idioms side by side in its comments:

src/rhf-profile.tsxtsx
import { FieldArray, Form, useForm } from "@shadow-js/forms";
import { z } from "zod";
import { standardSchema } from "@shadow-js/forms/standard-schema";
import { fieldArrays } from "@shadow-js/forms/arrays";

const schema = z.object({
  email: z.string().email("Enter an email"),
  tags: z.array(z.object({ label: z.string().min(2, "Too short") })),
});

export type ProfileData = z.output<typeof schema>;

export function Profile(props: { onSave?: (data: ProfileData) => Promise<void> }) {
  const form = useForm({
    features: [fieldArrays],
    resolver: standardSchema(schema), // resolver: zodResolver(schema)
    defaultValues: { email: "", tags: [{ label: "" }] },
    onSubmit: async (data) => {
      await props.onSave?.(data); // handleSubmit(onValid)
    },
  });
  const email = form.field("email"); // register("email")
  return (
    <Form form={form}>
      <label for={email.id}>Email</label>
      <input {...email.props()} />
      <p role="alert">{email.error()}</p>
      <FieldArray form={form} name="tags">
        {(row) => (
          <div>
            <input aria-label="Tag" {...row.field("label").props()} />
            <button type="button" onClick={() => row.remove()}>
              Remove
            </button>
          </div>
        )}
      </FieldArray>
      <button type="button" onClick={() => form.fields("tags").append({ label: "" })}>
        Add tag
      </button>
      <button type="submit">Save</button>
    </Form>
  );
}

Server errors from a Dusk server function go through applyServerErrors; the guide shows the round trip in Server functions as the submit handler.