Core Concepts
GForm, formKey, the GInput element render prop, and how form state works.
Everything in gform-react revolves around three ideas: a typed form interface, the GForm
container, and GInput fields identified by a formKey. This page explains how they fit
together.
The form interface
You describe your form as a TypeScript interface - keys are field names (formKeys), values are the
field value types. This single type drives the typing of state, toRawData(), and validators.
interface AddGoalForm {
title: string;
deadline: string;
status: "on-track" | "ahead";
}GForm and formKey
GForm<T> is the container. Each GInput declares a formKey that must be a key of T. The
formKey is also used as the input's name, which is how Server Actions read values from
FormData.
<GForm<AddGoalForm> validators={validators} onSubmit={handleSubmit}>
<GInput formKey="title" required element={/* … */} />
</GForm>Pass validators by a stable reference - declare it at module level, or wrap it in useMemo.
A new object on every render re-registers the fields and defeats the optimization.
The element render prop
By default - when you don't pass element - GInput renders a native <input>, fully wired:
value, handlers, name, and aria attributes are all applied for you. For simple fields that's
all you need.
To render anything else, provide an element function that receives:
input- the field's live state (value,error,errorText,formKey, …).props- the props you must spread onto your control (<input {...props} />): gform's wiring plus any extra props you set onGInput(see pass-through props).
This is what lets gform-react work with any UI: a native <input> with your own markup around
it, a <select>, <textarea>, or a component from MUI, PrimeReact, etc. The full prop table and
per-type value typing live on the GInput page.
<GInput
formKey="title"
required
element={(input, props) => (
<label>
<span>Title</span>
<input {...props} />
{input.error && <small>{input.errorText}</small>}
</label>
)}
/>Every other prop passes through
GInput accepts any attribute the rendered control does. Everything you put on GInput that
gform doesn't consume itself (formKey, element, validatorKey, fetch, fetchDeps,
debounce) is forwarded into props - the {...props} spread delivers it to your
control. So attributes like placeholder, autoComplete, className, or id go on GInput,
not on the element inside the render prop:
<GInput
formKey="username"
required
placeholder="username"
autoComplete="username"
element={(input, props) => (
<label>
{/* props carries placeholder + autoComplete (and the required constraint) */}
<input {...props} />
{input.error && <small>{input.errorText}</small>}
</label>
)}
/>This keeps a field's entire configuration in one place on GInput and lets you reuse the same
element function across fields.
A few props are managed by gform and can't be overridden this way: name is always the
formKey, value and checked are controlled by form state, and ref, aria-invalid, and
aria-required are injected. Handlers are chained, not replaced - an onChange, onBlur,
or onInvalid you pass to GInput runs after gform's own handler.
Typed for any control
props is typed so it can be spread onto an <input>, <select>, or <textarea> without a cast:
<GInput
formKey="status"
value="on-track" // initial value
element={(input, props) => (
<select {...props}>
<option value="on-track">On track</option>
<option value="ahead">Ahead</option>
</select>
)}
/>props already includes the onChange and ref gform needs - don't override them in the spread.
If you need your own handler, pass onChange/onBlur to GInput itself and gform chains it
after its own.
Form state
GForm can take a function child that receives the live state. Use it to read validity, read
values, or disable the submit button.
<GForm<AddGoalForm> validators={validators} onSubmit={handleSubmit}>
{(state) => (
<>
<GInput formKey="title" required element={/* … */} />
{/* disable from form state - not manual value checks */}
<button disabled={state.isInvalid}>Add Goal</button>
</>
)}
</GForm>The state object exposes:
| Member | Description |
|---|---|
state.<formKey>.value | The typed value of a single field. |
state.<formKey>.error | Whether the field currently has a validation error. |
state.<formKey>.errorText | The current error message. |
state.isValid / state.isInvalid | Single source of truth for whole-form validity. |
state.toRawData(options?) | { [formKey]: value } for the whole form. |
state.toFormData(options?) | A FormData instance (includes file fields). |
state.toURLSearchParams(options?) | A URLSearchParams instance. |
state.checkValidity() | Runs all validators and returns whether the form is valid. |
state.dispatchChanges(changes) | Programmatically update one or more fields. |
Reading the submitted data
Use toRawData() to read the whole form - no casts needed. Spread it to add extra fields:
const submit = (state: GFormState<AddGoalForm>) => {
const goal = { ...state.toRawData(), progress: 0 };
save(goal);
};You can also transform individual fields inline:
state.toRawData({ transform: { title: (v) => v.trim() } });onSubmit semantics
<GForm
validators={validators}
onSubmit={(state, e) => {
e.preventDefault(); // gform does NOT auto-preventDefault
save(state.toRawData()); // only runs when the form is valid
}}
>Two important guarantees:
onSubmitonly fires when the form is valid. An invalid form never reaches your handler.- gform does not call
e.preventDefault()for you. This is deliberate - it's what enables native<form>submission and Next.js Server Actions. CallpreventDefault()yourself when you handle submission in JavaScript. (For Server Actions you useactioninstead and must notpreventDefault- see Server Actions.)
Programmatic updates
To update a field from code and optionally re-run validation:
state.title.dispatchChanges({ value: "New title" }, { validate: true });Next
- Validation - native constraints, custom messages, and async rules.
- Disabling Submit - the
state.isInvalidpattern in detail.