Three pupas, a new green chrysalis coloration, one that’s about ready to emerge, and a butterfly that’s already come out
Tutorials |

The Evolution of Data Mutations Using Forms in React

Antony

Antony

January 30, 2025

Forms are integral to many web applications since they offer controls for collecting and submitting user information. They cover many use cases, from user registration to feedback submission, while allowing users to perform actions like submitting search queries, configuring settings, or applying filters. Forms are also one of the main ways to send data from the client to the server, usually via HTTP requests (like POST or GET). When a form submission modifies the data on the server, then it is referred to as a mutation. This communication is essential for most dynamic interactions on the web.

In this article, we will dive deep into the evolution of how forms perform mutations on the web, focusing on the usage of forms in React. We shall look into some of the best practices when working with forms in React, how some browser default behaviour has made it into the core of the React library in the React 19 release, and how developers can leverage these features to improve the user experience and accessibility of web applications.

Good ol' HTML Forms

Before we dive into how to handle forms in React, let us first brush up on how forms work in plain HTML.

HTML forms were officially introduced in the HTML 2.0 specification in 1995. Various input types such as text, password, radio buttons, and others, were defined to facilitate user interaction on the web. The introduction of HTML5 in 2014 brought several enhancements to forms, including new input types (email, url, date, time, number, range, color, and search), new attributes (required, pattern, min, max, step, multiple, and placeholder), and new elements (datalist and output). Today, forms are powerful and help us to collect varied types of user information.

Once this information is collected, it needs to be submitted to the server for storage or processing. This is achieved through an HTTP request, typically using either the GET or POST method. The method attribute on the <form> element determines how the data will be sent to the server, either get or post (or dialog which is not relevant for this article), defaulting to get if no value is provided. The action property on the <form> element specifies the server URL where the form data (FormData is a set of key/value pairs representing form fields and their values) should be sent. The server responds with a fresh HTML page after the form is submitted to the action URL.

GET

When a form is submitted using the get method, the form data gets appended to the action URL with a ? separator as a query string. This method is suitable for sending non-sensitive data but is limited by URL length restrictions on the amount of data sent to the server. An example of this is a filter checkbox on an e-commerce site:

When a user selects the first checkbox and clicks on the "Apply filters" button to submit the form, the key-value pair of the selected checkbox is URL-encoded as a query param and appended to the action URL, and the browser sends an HTTP GET request to the server.

The only difference between a form get submission and an a link href is that the user is in charge of supplying the information on the form, otherwise, both are standard web navigation. The above action is similar to the user initiating the navigation by directly clicking on the link:

One of the limitations of using get is that it should only be used to request data from the server, and we cannot attach any data to the request, which is crucial in collecting user information (in technical terms, performing a data mutation). To submit content to the server alongside the HTTP request we have to change the form method.

POST

Submitting a form using the post method allows the form data to be sent to the server as the HTTP request body. This allows for data mutations where a user can create, update or delete data on the server.

When a user fills in their name and submits the above user registration form, the browser will encode the form data (key-value pair) into the request body and send it to the server via an HTTP POST request by navigating to the action URL. Here is an example of how the server could listen to POST requests sent to the action endpoint (the first code snippet uses the PHP scripting language, while the second snippet uses the Hono JavaScript web framework):

When the server responds with a redirect, the browser triggers a new GET request to the redirect URL to fetch the fresh HTML to display. The browser will display a loading spinner (in most browsers, the favicon will turn into a spinner) when the form submission is in progress, and the submission (navigation) will result in a full-page reload of the browser. The browser is wholly responsible for providing client-side UI feedback (pending UI). It is important to note that full-page reloads are usually undesirable.

AJAX & jQuery

In order to build rich, dynamically generated, and interactive web applications, it was important to avoid full-page reloads and customize the UI feedback on the client when performing mutations. AJAX (Asynchronous JavaScript and XML) was created in 2005 to address this problem. It uses an XMLHttpRequest (XHR) to exchange data between the client and the server without reloading the whole page. Whereas XHR has a verbose and clunky API, jQuery, released in 2006, drastically improved the ergonomics of working with AJAX requests.

However, to use AJAX to submit the form, we must intercept the form submission event and prevent the browser from handling it (which would otherwise reload the page). Instead, we use JavaScript to serialize the form and send the data to a server endpoint. That explains the common usage of the well-known event.preventDefault() line of code prevalent throughout web development history.

The downside of this approach is that calling event.preventDefault() causes us to opt out of all the goodies that the browser provides by default. This means that we have to handle pending UI manually, update the UI after submission and handle cancellations and race conditions. We also have to ensure that we do not break the "back" button functionality, interfere with accessibility or create performance bottlenecks.

React v19 helps with these issues, but let us see how we got here.

Early Days of React

When the React library emerged in 2013, jQuery was one of the most popular tools for building web applications. React was commonly used with client-side routing libraries like React Router to create single-page applications (SPA). However, regarding form handling, there wasn't much difference in how data mutations were managed compared to using AJAX with jQuery. Although fetch replaced AJAX, developers still had to handle pending states, errors, optimistic updates and sequential requests with no additional help from the library.

A major difference, however, lies in how form state is handled in React. With jQuery, the application state lives inside the DOM, and the DOM is the "single source of truth" for the form state. React also supports form elements that manage their internal state via the DOM, and React components that render such elements are referred to as uncontrolled components. Refs are then used to retrieve form values from the DOM elements during submission. Since refs provide an imperative escape hatch to interact with the form elements outside React's control, React recommends a declarative approach. This is achieved using controlled components, where the React state is the "single source of truth" because the React component that renders the form also controls its behaviour on subsequent user input. Controlled inputs accept their current values as React props and have an onChange event handler to update those values when user input changes.

For a long time, React developers were split between these two approaches to handling forms in React and this even showed up in two of the most popular React form libraries, with Formik using controlled components and React Hook Form using uncontrolled components by default. While controlled form components come with the trade-off of more boilerplate and performance issues, uncontrolled components are harder to synchronize with other application state and working with refs was more complicated before React v19. This forced developers to rely on form libraries especially when working with complex forms.

The Remix Influence

During the React Conf 2024 keynote, Andrew Clark noted that the Remix web framework inspired a resurgence of action-based APIs in JavaScript frameworks as a pattern for client-server communication. He adds that this pattern fits perfectly into the React programming model. Therefore, the React team built action-based APIs into React to integrate with features like Streaming, Suspense and Transitions directly. To fully benefit from these features, web frameworks will build upon them as primitives and provide additional functionality. But despite this, actions are not exclusive to server-based frameworks - they also work on the client, even without a server framework.

Although this article does not delve into how server frameworks use these new APIs, it explains how these APIs change the way we handle forms on the client, which remains the same whether the application is fully client-side or server-rendered.

React 19

React 19 introduces significant changes in form data handling with new form features. The most notable change is the option to provide a function to the action property of <form> elements. When a URL is passed to action, the form will behave like the normal HTML form discussed above. However, when a function is passed to action, React will call it when the form is submitted, passing it a single argument containing the FormData of the submitted form. This function is referred to as the form Action and it may be synchronous or asynchronous. The form Action function can also be passed to the formAction prop on a <button>, <input type="submit">, or <input type="image"> element. Actions are a general pattern for asynchronous data updates in response to user input. Actions build on the HTML form API and React Transitions (by adding support for async functions).

NOTE: It is common to refer to React conceptual patterns or key terms using capitalized words, for example, "Action" instead of "action". This helps distinguish these important terms and emphasize that they have special meaning in the context of React.

NOTE: When a function is passed to the action or formAction prop, the HTTP method used to submit the data will be POST, regardless of the value of the method or formMethod prop.

NOTE: The React team recommends using the suffix Action when naming actions, similar to how we use on or handle prefix when naming event handlers.

React now embraces using the built-in HTML <form> element for submissions, embracing progressive enhancement since forms can work even without JavaScript. Also, using the native FormData browser API aligns React more to web standards, eliminating the need for abstractions and custom boilerplate.

NOTE: The default button type is submit which might be problematic for this paradigm since all buttons inside a form would implicitly trigger a form submission. It is recommended to use an ESLint rule to protect against this.

React also supports using Actions outside of <form> elements by allowing us to call the async submission function inside a React Transition. The Transition also provides us with an isPending flag that indicates whether it is pending, so we no longer need to track this ourselves. The ability to use async/await in Transitions was added in React 19.

A React "Action" is a function that triggers a Transition.

When a form Action succeeds, React will automatically reset the form's uncontrolled inputs. It's the same as calling form.reset() manually, except React handles the timing of when the reset happens, which is tricky or impossible to get exactly right. A new API requestFormReset(form), has also been added to React DOM to support resetting the form manually.

One benefit of using Actions is that React manages the life cycle of the data submission for us, allowing us to show pending UI and access the current state and response of the Action.

React 19 also introduces several new Hooks to help handle common cases for Actions and form Actions.

useFormStatus

This Hook provides status information on the last form submission and should only be called in a component rendered within a <form> element. It is useful in providing the pending state when working with Server Functions since forms will automatically wrap Server Functions in Transitions. Otherwise, the same functionality can be achieved in client components using the isPending flag from the useTransition hook. We can wrap the <form> in an Error Boundary to handle any errors thrown by the form Action.

useFormStatus reads the status of the parent <form> as if the form was a Context provider.

useActionState

This Hook allows us to update the state based on the result of a form Action. It accepts an Action function and wraps it in a Transition, returning the wrapped Action, an isPending flag, and the current state returned by the Action. The function passed to useActionState receives an extra argument, the previous or initial state, as its first argument, which makes its signature different than if it were used directly as a form Action without using useActionState.

useOptimistic

This Hook allows us to optimistically update the UI by showing a different state when the async action is underway. This state is automatically reverted once the final state commits, giving the impression of speed and responsiveness.

Conclusion

While this article only focused on how the new form features in React 19 enhance form data handling in client-only applications, these features are more interesting when paired with a React Server Components framework, like Next.js, since the React team has restructured the library to be able to work naturally and by default on the server. In this article, we submitted our form to a backend endpoint, but with React Server Components, React introduces first-class support for using Server Functions as form Actions. Server Functions are exposed server endpoints that can be invoked from anywhere in the client code. When the user submits the form, a network request is made to the Server Function. These frameworks enable developers to implement progressive enhancement, ensuring that applications remain functional without client-side JavaScript.

At Peerigon, we are experts in React and web technologies, with over 10 years of experience building projects on the web and particularly with React. We offer consulting, custom software development, and even React training workshops for teams that want to build React apps like pros. Feel free to reach out to us, and let's make something great together.

🤖 Statement about usage of AI in this article: This article was written by humans (thanks for the feedback Leonhard!), including the title, concepts, code samples. However we used AI to enhance the style of writing.

Header Photo by Suzanne D. Williams on Unsplash

Forms

React

Mutations

Web App Development

Remix

Actions

Optimistic UI

Read also

Dark, moody background of blueberry muffins on a cooling rack with a tablet displaying the Konsens app design layered over the image.

Klara, 12/05/2024

Accessibility – An Essential Ingredient in the Batter or the "Icing" on the Cake?

Web Accessibility

Post Mortem

Konsens

Digital Inclusion

Web Development

Digitale Barrierefreiheit

Go to Blogarticle

Francesca, Ricarda, 11/21/2024

Top 10 Mistakes to Avoid When Building a Digital Product

MVP development

UX/UI design

product vision

agile process

user engagement

product development

Go to Blogarticle

Leonhard, 10/22/2024

Strategies to Quickly Explore a New Codebase

Web App Development

Consulting

Audit

Go to Blogarticle