The Form State
The form state object - per-field value/error/errorText and whole-form validity, the ways to access it (onSubmit, the render prop, stateRef), and how to serialize it with toRawData, toFormData, and toURLSearchParams.
A GForm owns a single state store for the whole form. Every GInput registers its slice, and
the state object is your typed, read-only window into it: the current value of each field, whether
it's valid, and the methods to serialize or update it.
What's on the state object
| 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 field's current error message. |
state.isValid / state.isInvalid | Single source of truth for whole-form validity. |
state.checkValidity() | Runs every validator and returns whether the form is valid. |
state.toRawData() / toFormData() / toURLSearchParams() | Serialize the form - see Serializing the form. |
state.dispatchChanges(changes) | Update one or more fields from code - see Dispatching Changes. |
Each field slice is typed from your form interface, so state.<formKey>.value has the right type
(type="number" gives a number, type="checkbox" a boolean, and so on).
Accessing the state object
You receive the same state object three ways - reach for whichever fits where you are:
- The
onSubmit(andonReset,onChange) handler -onSubmit={(state, e) => …}hands you the state when the event fires. - The render prop -
GForm's function child{(state) => …}re-runs on every change, so it's the live read for your JSX. - The
stateRefprop - a ref kept pointed at the current state, for an imperative read outside render (a timer or callback).
When a field lives several components down, don't drill state through props - use the
useFormSelector hook to subscribe to just the slice you need, so
only that component re-renders.
Serializing the form
When a form submits, you need its values in some concrete shape - a JSON object for an API, a
FormData for a multipart upload, or a query string for a GET request. The state object gives
you three methods for exactly that:
| Method | Returns | Reach for it when… |
|---|---|---|
state.toRawData() | a plain object { [formKey]: value } | sending JSON to an API, or building a domain object. Keeps native types (File, number, boolean). |
state.toFormData() | a FormData instance | multipart uploads or file fields, and Next.js Server Actions. Web only. |
state.toURLSearchParams() | a URLSearchParams instance | a GET request or a shareable/bookmarkable query string. |
toRawData - a plain object
The default way to read the form. Values keep their JavaScript types, so a number field is a number
and a checkbox is a boolean - no parsing needed. Spread it to add fields the form doesn't own:
onSubmit={(state, e) => {
e.preventDefault();
const payload = { ...state.toRawData(), createdAt: Date.now() };
await fetch("/api/goals", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}}toFormData - multipart & files
toFormData() builds a FormData from the rendered <form>, keyed by each field's formKey. Because
it reads the real DOM form, file inputs come through as actual File objects - which is what you
want for uploads. Use it for multipart/form-data requests and Next.js Server Actions:
onSubmit={async (state, e) => {
e.preventDefault();
await fetch("/api/upload", { method: "POST", body: state.toFormData() });
// don't set Content-Type - the browser sets the multipart boundary for you
}}toFormData() depends on a native <form> element, so it's not available on React Native.
RNGFormState exposes toRawData() and toURLSearchParams() instead - see
React Native. For Server Actions you usually submit via the action prop
rather than calling toFormData() yourself - see Server Actions.
toURLSearchParams - query strings
toURLSearchParams() is built from toRawData() and then handed to new URLSearchParams(...), so
every value is stringified (42 → "42", true → "true"). That makes it perfect for a GET
search/filter form whose state should live in the URL - shareable and bookmarkable - and a poor fit
for files (a File won't serialize to anything useful). Call .toString() for the query string:
onSubmit={(state, e) => {
e.preventDefault();
const params = state.toURLSearchParams();
router.push(`/search?${params.toString()}`); // /search?q=shoes&size=42&inStock=true
}}Shaping the output with options
All three methods take the same options object, so once you know one you know all three:
{
include?: (keyof T)[]; // only these fields (others dropped)
exclude?: (keyof T)[]; // drop these fields
transform?: { [key in keyof T]?: (value) => any }; // map a value before it's written
}include wins over exclude if you pass both, and transform runs on top of whichever set remains.
Drop a field, keep only a few, or rewrite a value on the way out:
// Only two fields
state.toRawData({ include: ["email", "name"] });
// Everything except internal fields
state.toFormData({ exclude: ["_csrf"] });
// Map values before serializing
state.toURLSearchParams({
transform: {
q: (value) => value.trim().toLowerCase(),
tags: (value) => value.join(","),
},
});See all three side by side
This form renders each representation live as you type, so you can watch how the same state turns into
an object, FormData, and a query string - and how types are preserved or stringified:
Type in the fields and toggle the checkbox: in toRawData() the age stays a number and the checkbox
a boolean, while toURLSearchParams() shows them as strings.
toRawData() and toURLSearchParams() read field state, so an unchecked box is newsletter: false.
toFormData() reads the native <form>, and the browser omits unchecked checkboxes from FormData
entirely - so the key simply isn't there until it's checked. That's standard FormData behavior,
worth knowing if your backend expects the key to always be present.
Next
- Dispatching Changes - update fields programmatically and optionally re-validate.
- GForm - every prop on the form component, including
stateRef.