React Package

useForm()

Reference for the useForm() hook

Overview

declare namespace FormController {
  export type Configs<TOutput extends object> = {
    initialData?: DeepPartial<TOutput>;
    validationSchema?: StandardSchemaV1<unknown, TOutput>;
    equalityComparators?: Map<any, (a: any, b: any) => boolean>;
  };
}
export function useForm<TOutput extends object>(
  formConfigs: FormController.Configs<TOutput>,
  hookConfigs?: {
    validateMode?: "onChange" | "onBlur" | "onSubmit";
    revalidateMode?: "onChange" | "onBlur" | "onSubmit";
  },
);
  • TOutputrepresents the final validated shape of your form data. It defines the full object structure that the form is expected to produce after successful validation and submission.

useForm() is a thin React binding around the headless FormController (form.controller).

It creates the controller once, keeps it stable for the component's lifetime, and exposes a set of opt-in watcher hooks that allow React components to subscribe to specific form mutations.

At runtime, useForm():

  • Instantiates a single FormController and preserves it via useState
  • Subscribes React only to submission lifecycle changes ("submissionStatusChange" event)
  • Exposes the controller directly for imperative and declarative operations
  • Provides scoped watcher hooks (watchValues(), watchIssues(), watchEvent()) to opt into reactivity where needed
  • Internally uses useSyncExternalStore() to safely bridge the controller's event system into React without breaking concurrent rendering guarantees

Configurations

formConfigs!
FormController.Configs<TOutput>
The configuration object passed to internal FormController constructor. It defines the initial data, validation schema, and equality comparators for the form.
hookConfigs.validateMode?:
'onChange' | 'onBlur' | 'onSubmit'
Determines when field validation is triggered. Options:
  • "onChange": Validate on every value change (default)
  • "onBlur": Validate when a field loses focus
  • "onSubmit": Validate only on form submission
hookConfigs.revalidateMode?:
'onChange' | 'onBlur' | 'onSubmit'
Determines when field revalidation is triggered after the very first submission attempt. Options:
  • "onChange": Revalidate on every value change (default)
  • "onBlur": Revalidate when a field loses focus
  • "onSubmit": Revalidate only on form submission

Type Reference

useForm() returns an object with the following shape:

export interface UseForm<TOutput extends object> {
  formConfigs: FormController.Configs<TOutput>;
  hookConfigs: {
    validateMode: "onChange" | "onBlur" | "onSubmit";
    revalidateMode: "onChange" | "onBlur" | "onSubmit";
  };
  controller: FormController<TOutput>;
  path: FieldPathBuilder<TOutput>;
  watchValues: () => DeepReadonly<DeepPartial<TOutput>>;
  watchIssues: () => StandardSchemaV1.Issue[];
  watchEvent: (eventName: string, listener?: (payload: any) => void) => void;
}
import { type UseForm } from "@goodie-forms/react";
// ^ Library exports UseForm for easy typing in your use cases

interface MyProps {
  form: UseForm<MyFormData>;
  // ^ Such as a child component that receives the form as a prop
}

The Key Design Principle: Explicit Reactivity

useForm() does not behave like a traditional "form state hook". There is:

  • ❌ No automatic re-render on every keystroke
  • ❌ No large object diffing inside React
  • ❌ No duplicated state between React and the controller

Instead, you explicitly decide where React should subscribe to the form.

@goodie-forms/react exposes multiple granular hooks and components so you can bind React to exactly the slice of state you need:

  • 📦 <FieldRenderer />: A declarative reactive boundary that automatically subscribes to the field it renders. This is the preferred way to build your forms.
  • useFormField(): Establishes a scoped connection to a specific field, giving you the field API without subscribing to unrelated changes.
  • useFieldValue(): Reactively tracks only that field's valuenothing else in the form can trigger a re-render.
  • useFieldIssues(): Reactively tracks validation state for a single field.
  • 👁‍🗨 form.watchValues(): Re-renders the calling component when any form value changes. Useful for previews, summaries, or debugging views.
  • 👁‍🗨 form.watchIssues(): Re-renders when validation issues change. Ideal for global error banners or submit-state UI.
  • 👁‍🗨 form.watchEvent(): Lets you subscribe to any internal controller event with full control.

This model keeps React renders surgically precise:

  • Field components update independently from each other
  • Large forms avoid cascading renders
  • React only observes state you explicitly opt into
  • The FormController remains the single, framework-agnostic source of truth

In other words, React is used as a view subscription layer, not as the form state container.


Watchers

👁‍🗨 watchValues()

const values = form.watchValues();
//    ^? DeepReadonly<DeepPartial<TOutput>>

Subscribes the calling component to all form value changes.

  • Triggers a re-render whenever any field value is updated
  • Returns the current full form data object (TOutput)
  • Uses useSyncExternalStore() internally for concurrent-safe subscriptions
  • Does not subscribe to validation or submission state

Use when:

  • Rendering a live preview of the form
  • Building value-driven UI outside of individual fields
  • Inspecting the entire form state

👁‍🗨 watchIssues()

const issues = form.watchIssues();
//    ^? StandardSchemaV1.Issue[]

Subscribes the calling component to validation issue changes across the form.

  • Triggers a re-render whenever any field's issues are updated
  • Returns the controller's issue list
  • Independent from value updateschanging a value alone will not re-render unless it affects validation

Use when:

  • Rendering error summaries
  • Disabling/enabling submit UI based on validity
  • Showing global validation feedback

👁‍🗨 watchEvent()

form.watchEvent(eventName, listener?);

Subscribes to a specific internal FormController event.

  • Re-renders the component whenever the given event fires
  • Optionally invokes a listener with the event payload
  • Works with any event exposed by form.controller.events
  • Provides the lowest-level reactive hook for custom integrations

Use when:

  • You need fine-grained control over when React updates
  • Integrating with non-standard workflows
  • Reacting to lifecycle events not covered by watchValues() / watchIssues()

Examples

📚 Basic Form Setup

import { useForm } from "@goodie-forms/react";
import z from "zod";

const LoginFormSchema = z.object({
  email: z.string(),
  password: z.string(),
});

type LoginForm = z.infer<typeof LoginFormSchema>;

export function Login() {
  const form = useForm({
    validationSchema: LoginFormSchema,
    initialData: { email: "", password: "" },
  });

  const handleSubmit = form.controller.createSubmitHandler(
    async (data) => {
      console.log("Submitted", data);
    },
    async (issues) => {
      console.error("Failed submission with issues", issues);
    },
  );

  return (
    <form onSubmit={handleSubmit}>
      {/* Fields rendered elsewhere (e.g. FieldRenderer) */}
      <button type="submit">Submit</button>
    </form>
  );
}

📚 Watching All Values

import { type UseForm } from "@goodie-forms/react";

function LivePreview({ form }: { form: UseForm<LoginForm> }) {
  const values = form.watchValues();

  return <pre>{JSON.stringify(values, null, 2)}</pre>;
}

📚 Watching Validation Issues

import { type UseForm } from "@goodie-forms/react";

function ErrorSummary({ form }: { form: UseForm<LoginForm> }) {
  const issues = form.watchIssues();

  const hasErrors = issues.length > 0;

  if (!hasErrors) return null;

  return <div>Please fix the highlighted errors.</div>;
}

📚 Analytics / Telemetry Hook

import { type UseForm } from "@goodie-forms/react";

function FormAnalytics({ form }: { form: UseForm<LoginForm> }) {
  form.watchEvent("fieldValueChanged", (path) => {
    // Lightweight event tap — no rerender logic needed
    analytics.track("form_field_changed", { path });
  });

  return null;
}

📚 Syncing With External State

import { type UseForm } from "@goodie-forms/react";

function SyncToStore({
  form,
  setDraft,
}: {
  form: UseForm<LoginForm>;
  setDraft: (data: LoginForm) => void;
}) {
  form.watchEvent("fieldValueChanged");

  const values = form.controller.data;

  useEffect(() => {
    setDraft(values);
  }, [values, setDraft]);

  return null;
}
Copyright © 2026