React Package

useFormField()

Reference for the useFormField() hook

Overview

export function useFormField<
  TOutput extends object,
  TPath extends FieldPath.Segments,
>(
  form: UseForm<TOutput>,
  path: TPath,
  registerConfig?: {
    defaultValue?: Suppliable<FieldPath.Resolve<TOutput, TPath>>;
    overrideInitialValue?: boolean;
  },
): FormField<TOutput, TPath> | undefined;
  • 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.
  • TPathrepresents the strongly-typed path (as segment tuples) pointing to a specific leaf inside TOutput.

useFormField is a React hook that allows you to access and interact with a specific form field within your components.

It will only re-render the component when the specific field's state changes, making it an efficient way to bind to form fields without causing unnecessary re-renders from other parts of the form.

It provides the field if it exists, or undefined if the field is not found in the form controller. This is particularly useful for building custom input components that need to bind to form fields without creating a separate field registration.
In cases where you want to register a field directly within the hook call, you can provide an optional registerConfig object that will be forwarded to the form controller's registerField() method for the specified path. This allows you to set up field registration and configuration in one step when using the hook.

Configurations

form!
UseForm<TOutput>
The form controller instance returned by useForm(). This is required to access the form's internal state and methods for the specified field.
path!
FieldPath.Segments
The field path to the specific field within the form's data structure.
registerConfig.defaultValue?:
Suppliable<FieldPath.Resolve<TOutput, TPath>>
The default value to be used for the field if no initial value is set.
registerConfig.overrideInitialValue?:
boolean
Whether to override any initial value that was set on the form controller, if defaultValue was used to set the field's initial value.

Type Reference

Return type of useFormField() may be non-nullable if registerConfig is provided, since the field will be registered if it doesn't already exist. However, it's generally recommended to handle the possibility of undefined in your components to avoid unexpected errors.

Here are the declaration signature overrides, right from the codebase:

packages/react/src/hooks/useFormField.ts
// May return undefined if the field doesn't exist during runtime
export function useFormField<
  TOutput extends object,
  TPath extends FieldPath.Segments,
>(
  form: UseForm<TOutput>,
  path: TPath,
): FormField<TOutput, FieldPath.Resolve<TOutput, TPath>> | undefined;
packages/react/src/hooks/useFormField.ts
// Shall not return undefined since the field will be registered if it doesn't already exist
export function useFormField<
  TOutput extends object,
  TPath extends FieldPath.Segments,
>(
  form: UseForm<TOutput>,
  path: TPath,
  registerConfig: Parameters<typeof form.controller.registerField<TPath>>[1],
): FormField<TOutput, FieldPath.Resolve<TOutput, TPath>>;

Examples

📚 Basic Controlled Input

Even though useFormField() can be used to hand craft controlled inputs, it's highly recommended to use the FieldRenderer component instead for better ergonomics and less boilerplate in most cases.
import { useForm, type UseForm, useFormField } from "@goodie-forms/react";

interface FormData {
  user: {
    email: string;
  };
}

function EmailInput(props: { form: UseForm<FormData> }) {
  const fieldPath = props.form.path.of("user.email");

  const emailField = useFormField(props.form, fieldPath);

  return (
    <div>
      <input
        type="email"
        value={emailField?.value ?? ""}
        onChange={(e) => emailField?.setValue(e.target.value)}
        onFocus={() => emailField?.touch()}
        onBlur={() => emailField?.validate()}
      />

      {emailField?.issues.map((issue) => (
        <p key={issue.id} style={{ color: "red" }}>
          {issue.message}
        </p>
      ))}
    </div>
  );
}

📚 Derived UI (Reactive Display)

function MyForm() {
  const form = useForm({
    /* ...omitting */
  });

  const firstnameField = useFormField(form, form.path.of("user.firstname"));
  const lastnameField = useFormField(form, form.path.of("user.lastname"));
  const passwordField = useFormField(form, form.path.of("user.password"));

  const fullName =
    `${firstnameField?.value ?? ""} ${lastnameField?.value ?? ""}`.trim();

  const passwordStrength =
    (passwordField?.value.length ?? 0 > 8) ? "Strong" : "Weak";

  return (
    <form>
      <div className="form-fields">{/* omitting FieldRenderers */}</div>

      <div className="status-hud">
        <p>Full Name: {fullName}</p>

        {passwordField?.value && <p>Password Strength: {passwordStrength}</p>}
      </div>
    </form>
  );
}

📚 Conditional Rendering (Dynamic Forms)

interface FormData {
  isCompany: boolean;
  company?: {
    name: string;
    taxId: string;
  };
}

function CompanySection(props: { form: UseForm<FormData> }) {
  const isCompany = useFormField(props.form, props.form.path.of("isCompany"));

  if (!isCompany.value) return null;

  return (
    <>
      <FieldRenderer
        form={props.form}
        path={props.form.path.of("company.name")}
        render={({ fieldProps }) => (
          <div>
            <label>Company Name</label>
            <input {...fieldProps} />
          </div>
        )}
      />
      <FieldRenderer
        form={props.form}
        path={props.form.path.of("company.taxId")}
        render={({ fieldProps }) => (
          <div>
            <label>Tax ID</label>
            <input {...fieldProps} />
          </div>
        )}
      />
    </>
  );
}

📚 Array Field (Dynamic List)

interface FormData {
  tags: string[];
}

function TagsInput(props: { form: UseForm<FormData> }) {
  const tagsField = useFormField(props.form, props.form.path.of("tags"))!;

  const addTag = () => {
    const newTag = prompt("Enter a new tag");
    if (newTag) {
      tagsField.modifyValue((tagsValue) => {
        tagsValue.push(newTag);
      });
    }
  };

  const removeTag = (index: number) => {
    tagsfield.modifyValue((tagsValue) => {
      tagsValue.splice(index, 1);
    });
  };

  return (
    <div>
      <label>Tags</label>
      <ul>
        {tagsField?.value?.map((tag, index) => (
          <li key={index}>
            {tag} <button onClick={() => removeTag(index)}>Remove</button>
          </li>
        ))}
      </ul>
      <button type="button" onClick={addTag}>
        Add Tag
      </button>
    </div>
  );
}

📚 Listening to Field Changes (Side Effects)

interface FormData {
  user: {
    email: string;
  };
}

function useAutoSave(props: { form: UseForm<FormData> }) {
  const emailField = useFormField(props.form, props.form.path.of("user.email"));

  React.useEffect(() => {
    if (emailField?.value) {
      console.log("Email changed:", emailField.value);
      beacon.send("form_email_changed", { email: emailField.value });
    }
  }, [emailField?.value]);

  return null;
}
Copyright © 2026