Server-Side Rendering (SSR)

INFO

React only for now: other framework bindings are planned, and Lit's own SSR story (@lit-labs/ssr) is a separate concern this guide doesn't cover.

@kintools/form-react's bindings render on the server without any extra setup. useWatch/Watch are backed by useSyncExternalStore with a proper getServerSnapshot, so a form built the normal way, following the Basic guide, just works.

field.id in server-rendered markup

field.id is a plain, module-level counter (let nextId = 0): stable across array reorders, which is what makes it a good React key (see Dynamic Arrays), but not seeded the same way on the server and the client. Rendering it into an actual DOM attribute, like an <input id={field.id}> / <label htmlFor={field.id}> pair, mismatches during hydration.

Use useId() for a DOM id instead, and keep field.id scoped to key:

function TextField(
  { api, label }: { api: FieldApi<string, unknown>; label: string },
) {
  const field = useWatch(api);
  const reactId = useId();
  const inputId = `${field.name}-${reactId}`;
 
  return (
    <div>
      <label htmlFor={inputId}>{label}</label>
      <input
        id={inputId}
        value={field.value}
        onBlur={field.handleBlur}
        onChange={(e) => field.handleChange(e.target.value)}
      />
    </div>
  );
}

Passing server-loaded data in as initialValue

When the real initial value is already available on the server (e.g. a Next.js Server Component that fetched it before rendering), pass it straight into initialValue on the client component that calls useForm. There's no placeholder to reconcile and no reset() call needed: the value is there before useForm ever runs.

// Server Component
export default async function Page() {
  const profile = await fetchProfile();
  return <ProfileForm initialValue={profile} />;
}
 
// Client Component
"use client";
function ProfileForm({ initialValue }: { initialValue: Profile }) {
  const form = useForm({
    initialValue,
    onSubmit: (form) => saveProfile(form.value),
  });
 
  return (
    <form onSubmit={form.handleSubmit}>
      <TextField api={form.field("firstName")} label="First name" />
    </form>
  );
}

This is the same "delay mounting the form" pattern from Async Initial Values, just with the server doing the waiting instead of a client-side loading state.

Full example

examples/nextjs in the repo is a minimal Next.js App Router app (a Server Component page rendering a client-component LoginForm) that exists specifically to exercise these bindings under a real SSR framework, not just a client-only dev server.

What's next