React Forms That Write Themselves

Shopify's engineering team builds a lot of React forms, and the open source Quilt repo holds the tools they use to do it. The repo contains 72 npm packages, including react-form, which aims to manage React forms "tersely and safely-typed with no effort using React hooks." This walkthrough covers the core hooks and shows how to wire up a basic form.

Working With useForm and useField

The react-form documentation splits its API into three sections: Hooks, Validation, and Utilities. Of the eight available hooks, the two you'll reach for most often are useForm and useField. useForm is a custom hook that manages the state of an entire form. It relies on many of the other hooks internally and returns an object containing all the fields needed to run the form. When paired with useField, which handles the state and validations of an individual input, you get sensible defaults for common form patterns without writing much boilerplate.

Stripping Out Old State Logic

Because useForm takes over form state management, you no longer need local useState calls or the handler functions that updated input values. The only thing you keep is the submit handler, which gets replaced by the one returned from useForm. After removing the previous state and handler logic from the component, React starts throwing ReferenceErrors for missing functions like handleTitleChange. This happens because the TextField components still reference those handlers. Temporarily remove the onChange events and the value prop from both components—they'll come back via the fields object provided by useForm. The Page component's onAction property also references the now-deleted handleSubmit. The useForm hook provides its own submit function that does the same job, so assign that to onAction for now. Finally, remove the useState import, since form state no longer lives in the component directly.

Bringing In the Hooks

With the old code cleaned out, import both hooks from react-form (which is already installed as a dependency in this setup): import { useForm, useField } from "@shopify/react-form”; Looking at the first useForm example in the documentation, you can see the hook accepts a configuration object that defines the form fields and an onSubmit function. The fields are each assigned their own useField() invocation. Without the value and onChange props, the text inputs no longer capture or display text. Those props now live inside the fields object. Adding a console log to inspect the object structure shows that each key (like description) contains everything needed, including onChange and value. Spread them onto each TextField component by destructuring the matching key from fields. The field object also exposes properties like reset and dirty, which come in handy once the submit function is connected.

Handling Submissions and Extra State

Now it's time to actually submit the form. Remove the quotes around submit assigned to the Page component's onAction prop. To verify the onSubmit function fires correctly, add a console log to inspect the fields object, then enter a title and description and hit Save.
Adding a Product Screen with a Title and Description fields
Adding A Product
Beyond the essentials, useForm provides two more features worth using: reset and dirty.

Clearing the Form After Save

The reset method clears the form and gives the user a blank slate for entering another item. Call it only after the field data has been sent to the backend and handled, but before the return statement. With that in place, submitting text clears the inputs as expected.

Disabling Save Until There's Something to Save

The dirty flag indicates whether any field has been changed. Use it to disable the Save button until the user types into one of the inputs. The Page component has a disabled property on its Save button; assign it the value !dirty. Since dirty starts as false, the inverted value enables the button only after an edit occurs.
Developer Tools screenshot showing the code disabling the save button.
Save Button Disabled
The Save button now stays disabled until the user types into either field, at which point it enables. A quick inspection in developer tools confirms the button's disabled state flips correctly.

Requiring Input Before Submit

Enabling the Save button as soon as the user types into either field exposes a problem: a product can be submitted without a title. The form should block submission until the Title field has some content, with an error message clarifying what’s missing.

To enforce this, pull the notEmpty hook from react-form. Adding it changes how useField is called—it now takes a configuration object with two keys:

  • value: tracks the field’s current input value
  • validates: runs validation logic against that value

Passing notEmpty into validates for the title field means the form rejects an empty title on submit and surfaces a required-field error to the user.

Add Product screen with Title and Description fields. Title field is empty and showing error message “field is required.”
Field is required error message

Testing the workflow—entering only a description, then clicking Save—confirms the validation fires and the form stays in a valid, expected state.

Where to Go From Here

The example pulls together the core behavior enabled by useForm and useField: stateful fields, dirty tracking, and declarative validation. Those two hooks carry considerably more capability beyond what this walkthrough covers, particularly when forms grow in complexity.

react-form is part of Shopify’s open source Quilt repository, which also includes several other React-focused hooks worth exploring:

  • react-graphql: type-safe, asynchronous GraphQL components for React
  • react-testing: utilities for testing React components against Shopify’s conventions
  • react-i18n: internationalization helpers for translations and formatting

For a practical starting point, the hook-based API keeps form code close to the component logic it drives, making state and validation behavior explicit rather than scattered through the tree.