gform-react
Back to Guides

Nested Forms

Spread form fields across any number of components using context and useFormSelector.

A GInput doesn't have to be a direct child of GForm. Because gform-react uses React context, a GInput works anywhere inside the GForm subtree - even several components deep. This lets you break large forms into focused, reusable section components.

Fields in child components

Move groups of fields into their own components. As long as they render inside the GForm, they join the same form state and validation.

ProfileForm.tsx
import { GForm, GInput, GValidator, type GValidators } from "gform-react";
 
interface ProfileForm {
  firstName: string;
  lastName: string;
  street: string;
  city: string;
}
 
const validators: GValidators<ProfileForm> = {
  "*": new GValidator().withRequiredMessage("Required"),
};
 
// A reusable text field.
function Field({ formKey, label }: { formKey: keyof ProfileForm; label: string }) {
  return (
    <GInput
      formKey={formKey}
      required
      element={(input, props) => (
        <label>
          <span>{label}</span>
          <input {...props} />
          {input.error && <small>{input.errorText}</small>}
        </label>
      )}
    />
  );
}
 
// A whole section, several components deep - still part of the same form.
function AddressSection() {
  return (
    <fieldset>
      <legend>Address</legend>
      <Field formKey="street" label="Street" />
      <Field formKey="city" label="City" />
    </fieldset>
  );
}
 
export default function ProfileForm() {
  return (
    <GForm<ProfileForm>
      validators={validators}
      onSubmit={(state, e) => {
        e.preventDefault();
        console.log(state.toRawData());
      }}
    >
      <Field formKey="firstName" label="First name" />
      <Field formKey="lastName" label="Last name" />
      <AddressSection />
      <button>Save</button>
    </GForm>
  );
}

Reading fields with useFormSelector

Any component inside the form can subscribe to a slice of form state with useFormSelector - without prop-drilling. It re-renders only when the selected slice changes, preserving gform's minimal re-render guarantee.

import { useFormSelector } from "gform-react";
 
function CityBadge() {
  const city = useFormSelector((state) => state.fields.city);
  return <span>Selected city: {String(city?.value ?? "-")}</span>;
}
Why this stays fast

useFormSelector subscribes to just the slice you return. A component reading state.fields.city won't re-render when an unrelated field changes - which is how nested forms stay performant even when they're large.

Try it live

Loading playground…