gform-react
Back to Components

GForm

The typed form container - props, children modes, submission, onInit seeding, stateRef, and optimized mode.

GForm is where every form starts. It renders a real <form> element, owns the form's state store, and registers every GInput rendered beneath it. Type it with your form interface and state, toRawData(), and validators are all typed from that single source:

import { GForm, GInput, GValidator, type GValidators } from "gform-react";
 
interface SignUpForm {
  email: string;
  password: string;
}
 
const validators: GValidators<SignUpForm> = {
  "*": new GValidator().withRequiredMessage("Required"),
};
 
export function SignUp() {
  return (
    <GForm<SignUpForm>
      validators={validators}
      onSubmit={(state, e) => {
        e.preventDefault();
        api.signUp(state.toRawData()); // typed { email: string; password: string }
      }}
    >
      {(state) => (
        <>
          <GInput formKey="email" type="email" required />
          <GInput formKey="password" type="password" required minLength={8} />
          <button disabled={state.isInvalid}>Sign up</button>
        </>
      )}
    </GForm>
  );
}

Because it renders a native <form>, GForm also accepts every standard form attribute (className, action, method, noValidate, id, …) and forwards ref to the underlying HTMLFormElement.

Props

PropTypeDescription
childrenReactNode | ((state: GFormState<T>) => ReactNode)Static children, or a render function receiving live form state.
validatorsGValidators<T>formKey → GValidator map; "*" is the default for every field. Pass a stable reference.
onSubmit(state, e) => voidFires only when the form is valid. gform does not auto-preventDefault.
onReset(state, e) => voidRuns after a native form reset restores every field to its initial value.
onChange(state, e) => voidForm-level change handler - fires when any field changes.
onPaste(state, e) => voidForm-level paste handler.
onKeyDown / onKeyUp(state, e) => voidForm-level keyboard handlers.
onInit(state) => void | PartialForm<T> | Promise<>Runs once after mount; return partial field state to seed the form.
stateRefRefObject<GFormState<T> | undefined>A ref gform keeps pointed at the current form state.
optimizedbooleanDelegate change/blur/invalid to the <form> - see below. Becoming the default in a future release.

Every state argument above is the same GFormState<T> - isValid/isInvalid, toRawData(), toFormData(), dispatchChanges(), and a GInputState per field. See the API reference for the full shape.

Static children vs the render prop

children can be ordinary JSX, or a function that receives the live form state:

// Static children - passed through by reference
<GForm<SignUpForm> validators={validators}>
  <GInput formKey="email" type="email" required />
  <SubmitButton />
</GForm>
 
// Render prop - re-runs whenever any field changes
<GForm<SignUpForm> validators={validators}>
  {(state) => (
    <>
      <GInput formKey="email" type="email" required />
      <button disabled={state.isInvalid}>Sign up</button>
    </>
  )}
</GForm>

Use the render prop when something outside a field needs live form state - the classic case is <button disabled={state.isInvalid}>.

Static children render less

Static children are passed through by reference, so a keystroke re-renders only the GInput being typed in. The render-prop function re-runs on every field change - anything expensive inside it should be wrapped in memo, or should subscribe to just the state it needs via useFormSelector.

Submission

onSubmit(state, e) fires only when the form is valid - an invalid submit marks the failing fields with their error messages instead. Two things gform deliberately does not do:

  • It does not call e.preventDefault(). Call it yourself when you handle submission in JavaScript:

    onSubmit={(state, e) => {
      e.preventDefault();
      save(state.toRawData());
    }}

    Leaving native submission intact is what makes Server Actions work: there you pass action={formAction} instead of onSubmit and must not prevent the default.

  • It does not inject a submit button. A native <button> inside a form submits by default, so no type="submit" is needed - but a custom <Button> component usually renders type="button", in which case you must pass type="submit" explicitly. In development gform warns if the form has no submitter at all.

Resetting

A native <button type="reset"> inside the form (or calling reset() on the <form> element) restores every field to its initial value - the value prop, or whatever onInit seeded. Because gform owns each field's value, it intercepts the reset, restores its store, and the controlled inputs re-render to match (files included). The optional onReset(state, e) prop runs after the restore:

<GForm<SignUpForm> validators={validators} onReset={() => setServerError(null)}>
  <GInput formKey="email" type="email" required />
  <button>Sign up</button>
  <button type="reset">Reset</button>
</GForm>

A reset re-applies the validity captured at registration but does not re-run custom/async validators. For clearing to empty or setting specific values in place, use dispatchChanges. See Resetting a Form for the full rundown.

Form-level handlers

onChange, onPaste, onKeyDown, and onKeyUp mirror their native counterparts but receive (state, event) - useful for behavior that spans fields, like submit-on-Ctrl-Enter:

<GForm<NoteForm>
  onKeyDown={(state, e) => {
    if (e.ctrlKey && e.key === "Enter" && state.isValid) {
      e.currentTarget.requestSubmit();
    }
  }}
>

Seeding the form with onInit

onInit(state) runs once, right after the form mounts. Return a partial form - or a Promise of one - and gform merges it into the fields. This is the standard way to populate an "edit" form from an API:

<GForm<ProfileForm>
  onInit={async () => {
    const profile = await fetchProfile();
    return {
      name: { value: profile.name },
      city: { value: profile.city },
    };
  }}
>

The returned shape is { [formKey]: Partial<GInputState> } - usually just value, but you can also seed error/errorText if the server reports problems. For loading a single field (or reloading it when other fields change), use GInput's fetch prop instead.

Reading state outside render: stateRef

stateRef keeps a ref pointed at the current form state - useful in callbacks and effects that need to read the form without subscribing to (and re-rendering on) every change:

const formState = useRef<GFormState<DraftForm> | undefined>(undefined);
 
useEffect(() => {
  const id = setInterval(() => {
    const state = formState.current;
    if (state?.isValid) saveDraft(state.toRawData());
  }, 10_000);
  return () => clearInterval(id);
}, []);
 
return <GForm<DraftForm> stateRef={formState}>{/* fields */}</GForm>;

optimized mode

By default, every GInput wires up its own change, blur, and invalid handlers on the control it renders. That works fine, but a form with N fields ends up registering N sets of handler closures.

optimized switches GForm to event delegation: the change/blur/invalid handlers are registered once on the <form>, and gform routes each event to the right field by its name (which it sets to the field's formKey). You trade N handler sets for one - the behavior is identical.

<GForm<HugeForm> optimized>
  <GInput formKey="row-1" />
  <GInput formKey="row-2" />
  {/* …hundreds more, no per-field setup… */}
</GForm>

Why it matters

  • Fewer listeners and allocations. One delegated handler set instead of one per field - the form is lighter to mount and cheaper as fields are added and removed. The larger or more dynamic the form, the more it saves.
  • Same behavior, less work. Validation, onChange, submission gating, and the automatic aria-* wiring all behave exactly as they do without it - delegation only changes where the handlers live, not what they do.
Becoming the default

As of gform-react 3.1.0, a <GForm> without optimized logs a one-time, dev-only console notice: delegation will become the default in a future release. Adopt it now to get the new behavior early and silence the notice:

<GForm optimized> {/* … */} </GForm>

The notice fires once per session and never appears in production builds. optimized is a GForm prop - in 3.1.0 it is no longer set per-GInput.

Needs native form controls

Delegation depends on change/blur/invalid events reaching the <form>, so use optimized with fields that render native controls (<input>, <select>, <textarea>). File inputs are handled for you. UI-library fields (MUI, PrimeReact, …) work too - they wrap a real native control under the hood, which is the same element gform already spreads props onto, so the events still bubble up to the form.

React Native

On React Native use RNGForm from gform-react/native - same idea, but it needs an el prop (the container component, e.g. View) since there's no native <form>. See React Native.

Try it live

An edit form seeded asynchronously with onInit - the fields fill in once the (simulated) API call resolves:

Loading playground…