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
| Prop | Type | Description |
|---|---|---|
children | ReactNode | ((state: GFormState<T>) => ReactNode) | Static children, or a render function receiving live form state. |
validators | GValidators<T> | formKey → GValidator map; "*" is the default for every field. Pass a stable reference. |
onSubmit | (state, e) => void | Fires only when the form is valid. gform does not auto-preventDefault. |
onReset | (state, e) => void | Runs after a native form reset restores every field to its initial value. |
onChange | (state, e) => void | Form-level change handler - fires when any field changes. |
onPaste | (state, e) => void | Form-level paste handler. |
onKeyDown / onKeyUp | (state, e) => void | Form-level keyboard handlers. |
onInit | (state) => void | PartialForm<T> | Promise<…> | Runs once after mount; return partial field state to seed the form. |
stateRef | RefObject<GFormState<T> | undefined> | A ref gform keeps pointed at the current form state. |
optimized | boolean | Delegate 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 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 ofonSubmitand must not prevent the default. -
It does not inject a submit button. A native
<button>inside a form submits by default, so notype="submit"is needed - but a custom<Button>component usually renderstype="button", in which case you must passtype="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 automaticaria-*wiring all behave exactly as they do without it - delegation only changes where the handlers live, not what they do.
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.
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: