gform-react
Back to The Basics

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

MemberDescription
state.<formKey>.valueThe typed value of a single field.
state.<formKey>.errorWhether the field currently has a validation error.
state.<formKey>.errorTextThe field's current error message.
state.isValid / state.isInvalidSingle 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 (and onReset, 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 stateRef prop - a ref kept pointed at the current state, for an imperative read outside render (a timer or callback).
Reading state deep in the tree

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:

MethodReturnsReach 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 instancemultipart uploads or file fields, and Next.js Server Actions. Web only.
state.toURLSearchParams()a URLSearchParams instancea 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
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
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
}}
Web only

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
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:

Loading playground…

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.

Why the checkbox is missing from toFormData()

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.