The Future of Complex JSS Forms: New React 19 Hooks

React 19 is going to overhaul how you build and architect your XM Cloud multipage forms!

May 8, 2025

By Tyler Holmes

Sitecore JSS Forms Are About to Have a Drastic Change

Everyday it feels like we get information about a new major release from one of the many JavaScript frontend frameworks, and the React Team dropped some huge updates with the release of React 19 that will impact how you create custom multistep forms for your Sitecore projects.

Up until now, the best way to create a detailed custom form for Sitecore JSS would be creating a custom form hook, that you can then use to handle all the data aggregation, loading states, and submissions. This allowed for a consistent and reusable structure for all the forms across the site - it also allowed for easy integration with Sitecore Pages & Experience Editor. With the new React 19 update, creating JSS forms will be much simpler once Sitecore adopts Next.js 15 into JSS since Next.js 15 has native support for React 19.

React 19 provides a plethora of unique and new hooks that will improve how your forms will work. Your forms will have improved async transitions with the new concept of “Actions”, as well as the addition of 3 new hooks specifically for Form Management. React 19 also adds a bunch of other cool improvements and enhancements that will indirectly benefit complex workflows, like creating custom multipage & multistep forms with Sitecore JSS.

How Will React 19 Affect My XM Cloud Form Structure?

Not much! The foundation should be mostly the same for your super complex forms. Your goal should still be to both enhance the developer experience of working with smaller more manageable files—this allows for easy code splitting, and ensuring that your users are only loading what's need, when it's needed.

If everything is bundled up into one large file, when your users first arrive on the page they are loading content for all the final steps up front; if you have a conditional form, they would be loading steps they might never see. The best approach both before React 19 and after is to still break your components up and implement lazy loading via next/dynamic, which is an out-of-the-box Next.js component that dynamically loads components as needed.

Note: Implementing next/dynamic with Sitecore is a little different from a native Next.js project.

User Flow Diagram

The key idea is to break down your code into smaller, more modular pieces, but this doesn't mean you need to overcomplicate things on the Sitecore side. You still want to create only one rendering for the entire form. The goal should be to make the authoring experience should match the seamless multistep development process. To achieve this, you can create a single .tsx parent file that will be the container for all the child steps, and be the only component inside the component's folder that connects to Sitecore. In a Sitecore JSS project, all the sub-steps should be stored in a child components folder.

If you want to read more about how you should structure a complex form with Sitecore, you can read about it here!

React 19’s New Hooks & How They Will Interact With Sitecore

The changes to how hooks work in React 19 is going to really overhaul how you build your forms once JSS gets updated to use React 19 and Next.js 15. I want to cover what each of the new relevant hooks do and what some of the new hook concepts are that you can use to improve your Sitecore forms.

Quick Reference Guide to the New (and Old) React Hooks

Hook New in React 19 Type/Purpose Use Cases
useOptimistic Yes State Management Showing “optimistic” UI updates while async operations are pending
useTransition Yes Performance Marking state updates as non-urgent to keep UI responsive
useActionState Yes State Management Managing state for form actions with pending/success/error states
useFormStatus Yes Forms Accessing form submission status (pending, submitting)
useState No State Management Managing simple state values (strings, numbers, booleans, objects)
useEffect No Side Effect API calls, subscriptions, manual DOM manipulations, non-UI updates
useLayoutEffect No Side Effect DOM measurements and mutations that must run before browser paint
useRef No Reference Accessing DOM elements, storing mutable values that don't trigger re-renders
useReducer No State Management Complex state logic, state transitions, multiple sub-values, deep updates
useContext No Context Accessing shared values without prop drilling
useMemo No Performance Caching expensive calculations, preventing unnecessary re-computations
useCallback No Performance Memoizing functions to prevent unnecessary recreation

React 19 New Concepts: Actions & Async Transitions

The newest and biggest change that comes with React 19 is the new Actions API - This is going to make handling form submission in your XM Cloud headless app much easier. The old method of form submissions required you to write your own custom hook that managed loading spinners, disabling form submissions, error handling, and preventing multiple submission requests. Actions solves most of these issues by handling everything automatically. The biggest change with React 19 is now: Any function that causes an async state update (like submitting a form) will be classified as an “Action”, which will allow the code to be ran in a non-blocking way. This removes the need to manage the loading states, error flags or multiple requests manually since React 19 will handle everything for you.

For example: If your user submits a form that has its submission logic wrapped with an async transition, React will run that code in the background without freezing the UI and automatically track the progress of the submission. React will catch any errors via an error boundary and ensures that any sequential requests don't conflict, since each transition state update is handled in the submitted order.

What Is useTransition?

useTransition is a key part of the utilizing the new Actions API. In the past, i would have created a custom forms hook to handle everything that useTransition and useActionState gives us as new out-of-the-box hooks. The new hook gives us an isPending and startTransition items that can handle all our state changing.

You want to use useTransition when you need to handle background updates or manage visual state transitions. This is the most useful when the UI needs to continuously render while a less critical task is happening in the background. The best use case for useTransition is for cases like Lazy Loading, and non-urgent operations which aren't directly tied to forms, but still very useful to know about.

import { useTransition } from "react";
import { Text, RichText, useSitecoreContext } from "@sitecore-jss/sitecore-jss-react";

function SlowUpdateComponent({ fields }) {
  const { sitecoreContext } = useSitecoreContext();
  const [isPending, startTransition] = useTransition();

  const handleClick = () => {
    startTransition(() => {
      // Simulation of a background task
      setTimeout(() => {
        console.log("Background task completed");
      }, 2000);
    });
  };

  return (
    <div>
      <Text field={fields.Heading} tag="h2" />
      <button onClick={handleClick} disabled={isPending}>
        Start Background Task
      </button>
      {isPending && <Text field={fields.LoadingText} tag="p" />}
      <RichText field={fields.FooterText} />
    </div>
  );
}

export default SlowUpdateComponent;

The New Hooks: useActionState, useOptimistic, & useFormStatus

What Is useActionState? The useActionState hook is the bread and butter of the React 19 forms update, allowing you to easily manage the state of your form actions! It returns both a state value and a special action function you can attach directly to your form. When the form is submitted using the special action, the state will update automatically with the action that our function returns. This is a great way to handle the data for the form and means we no longer need to write an abundance of state management code. It also provides an isPending flag (similar to useTransition) to tell if the action is in progress. This allows us to easily do basic form tasks like showing a loading message or disabling the submit button while waiting for a response (although using useFormStatus is a better way of doing this)!

You want to use useActionState when you want to:

  • Store and display server responses (like error messages)
  • Update UI based on action results
  • Maintain state between submissions
import { useActionState } from "react";
import { useSitecoreContext } from "@sitecore-jss/sitecore-jss-react";
import { submitForm } from "./exampleCode";

const Form = ({ fields }) => {
  const { sitecoreContext } = useSitecoreContext();
  const [formAction, actionState] = useActionState(submitForm, null);

  const handleSubmit = (e) => {
    e.preventDefault();
    const data = new FormData(e.target);
    formAction(data);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="username" placeholder="Username" />
      <button type="submit" disabled={actionState.isPending}>
        Submit
      </button>

      {(sitecoreContext.pageEditing || actionState.isPending) && <Text field={fields.loadingText} tag="p" />}
      {(sitecoreContext.pageEditing || actionState.error) && <Text field={fields.errorText} tag="p" />}
      {(sitecoreContext.pageEditing || actionState.data) && <Text field={fields.successText} tag="p" />}
    </form>
  );
};

What Is useOptimistic?

The useOptimistic hook helps you implement what the React team calls “optimistic” UI updates. It lets you basically update the interface immediately by assuming your operation will succeed! Then, if it fails horribly, it will reconcile it later. This is a bit of a double edge sword for your users, who could see a “success” state that converts into a “failed” state - however the upside is how it makes the site feel unbelievably fast. useOptimistic requires an initial state and an update function as its inputs. When you call the updater function that useOptimistic returns, the state changes right away to the "optimistic" value you provide! Obviously, you would still perform the actual async operation (like an API call to Sitecore) in the background, but the user gets that instant feedback in the UI.

You want to use useOptimistic when you want to:

  • Show immediate feedback while async operations are in progress
  • Enhance user experience by reducing perceived latency
  • Handle long-running operations without blocking user interaction
import { useOptimistic, useState } from "react";
import { useSitecoreContext } from "@sitecore-jss/sitecore-jss-react";

function LikeButton({ postId, fields }) {
  const { sitecoreContext } = useSitecoreContext();

  const [likes, setLikes] = useState(42);
  const [optimisticLikes, addOptimisticLike] = useOptimistic(likes, (currentLikes, optimisticValue) => currentLikes + optimisticValue);

  async function handleLike() {
    // Immediately update UI optimistically (add 1 like)
    addOptimisticLike(1);

    try {
      // Perform the actual API call in the background, realistically this should be in its own function/file.
      const response = await fetch(`/api/posts/${postId}/like`, {
        method: "POST",
      });

      if (!response.ok) throw new Error("Failed to like post");

      // If successful, update the real state
      const data = await response.json();
      setLikes(data.likes);
    } catch (error) {
      // If the API call fails, the UI will automatically revert
      // to the real state (likes) when we update it
      console.error("Failed to like post:", error);
      setLikes(likes); // Reset to original value
    }
  }

  return (
    <div>
      <button onClick={handleLike} disabled={optimisticLikes > likes}>
        Likes: {optimisticLikes}
      </button>
      {(sitecoreContext.pageEditing || optimisticLikes > likes) && <Text field={fields.savingText} tag="span" />}
    </div>
  );
}

What Is useFormStatus?

The useFormStatus hook is all about giving you status updates for your form submission. It’s typically used inside a form (or any components you might have that are nested within a form) to check if the form is currently submitting and to get details about the last submission. The most common property you will use is probably pending, a boolean that is true while the form is being submitted (don't confuse this with isPending from useActionState). Using this, you can disable form inputs or show a loading indicator when an action is in progress.

The utility of useFromStatus might sound very similar to useActionState at first glance, but useFormStatus is always going to give you more in-depth information. Its entirely focused on specifically returning the form's status, while useActionState is targeted towards handling the form data.

import { useFormStatus } from "react-dom";
import { Text } from "@sitecore-jss/sitecore-jss-react";
import handleSubmit from "./handleSubmit";

// Form component
export default function UsernameForm({ fields }) {
  return (
    <form action={handleSubmit}>
      <div>
        <Text field={fields.label} tag="label" />
        <input
          type="text"
          id="username"
          name="username"
          //disabled={...} - Will be updated by useFormStatus in your implementation
        />
      </div>

      <SubmitButton fields={fields} />
    </form>
  );
}

function SubmitButton({ fields }) {
  const { pending, data } = useFormStatus();

  return (
    <div>
      <button type="submit" disabled={pending}>
        {pending ? fields.loadingText : fields.buttonText}
      </button>

      {pending && data && <Text field={fields.successText} tag="p" />}
    </div>
  );
}

You want to use useFormStatus when you want to:

  • Show loading indicators during submission
  • Access form data being submitted
  • Disable form elements while submitting

Recapping The Differences Between useActionState, useOptimistic, & useFormStatus

React 19 has some amazing form updates that will impact any headless Sitecore site that will want to update to Next 15 once Sitecore JSS embraces it. It's important that you understand how each part of the new hooks work to be able to build easy to maintain forms. useActionState is great for handling all your form data needs, useOptimistic is perfect for tricking the user into thinking you have the fastest website on the planet and useFormStatus allows you to track the updates and status’ of your form while you submit.

Tyler Holmes

Full Stack Developer | Sitecore Technology MVP

Tyler is an experienced Full Stack Developer who has worked on a plethora of unique projects. He possesses a deep understanding of Technical SEO, C#, and React/Next.js. Tyler is also a Sitecore Tech MVP!