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…
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:
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.