Skip to content

React v19 has been officially released

We highlight the differences between the old and new ways of coding in React

The latest version of React, v19, has been officially released. It includes a range of new features designed to simplify app development. In this post, we’ll walk through these new features and explore the differences compared to previous implementations without React 19.

We have a working demo, if you’d like to try it yourself.

Table of contents

  1. useTransition

  2. useActionState

  3. useFormStatus

  4. form actions

  5. useOptimistic

  6. use

  7. ref as a prop

  8. <Context> as a provider

  9. useDeferredValue initial value

  10. Support for document metadata

useTransition

We no longer need to manage loading states manually for handling async functions, thanks to the new useTransition hook. This hook takes care of managing the async call and its state, as demonstrated below.

Without useTransition

typescript
const [response, setResponse] = useState<number>();
const [isTransitioning, setTransitioning] = useState<boolean>();

const handleApiCall = async () => {
  setTransitioning(true)
  
  try {
    setResponse(await fetch(...));
  } catch (err) {
    // handle exceptions
  } finally {
    setTransitioning(false)
  }
};

return isTransitioning ? <div>Loading...</div> : ...

In this case, we still need to manage internal loading states, and the UI may become unresponsive at times due to multiple state updates. As you can see, we need to manually update the isTransitioning variable according to the state of the API request. Therefore, we need to set it to true before sending the request and to false after receiving a response.

With useTransition

typescript
const [response, setResponse] = useState<number>();
const [isTransitioning, startTransition] = useTransition();

const handleApiCall = () => {
  startTransition(async () => {
    setResponse(await fetch(...));
  });
};

return isTransitioning ? <div>Loading...</div> : ...

Calling startTransition manages the transition state of the async function and updates it based on the function’s execution. The transition state will automatically be set to true and revert to false once all Promises are settled. We don’t need to maintain isTransitioning because useTransition handles it automatically.

In addition, the useTransition hook ensures that the UI remains responsive and interactive, even while internal state updates are occurring in the background.

useActionState and useFormStatus

In the past, we had to create and manage internal states to handle form actions, as we saw earlier. To address this, React 19 has introduced two new powerful hooks that take care of managing internal form states for us.

Without useActionState and useFormStatus

typescript
// Todo.tsx
export const Todo = () => {
  const [tasks, setTasks] = useState<string[]>([])
  const [isHandling, setHandling] = useState<boolean>(false)
  const [task, setTask] = useState<string>('')
	
  const handleSubmit = async (e) => {
    e.preventDefault()
    
    setHandling(true)
    try {
      const response = await fetch(...);
		
      setTasks((oldState) => [...oldState, response]) 
      setTask('')
    } catch (err) {
      // handle exceptions
    } finally {
      setHandling(false)
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      ...
      <FormButton pending={isHandling}>Add</FormButton>
    </form>
  );
};

// FormButton.tsx
export const FormButton = ({ children, pending }) => (
  <button disabled={pending}>
    {pending ? "Please wait..." : children}
  </button>
)

As you can see, both are traditional React components used to implement a minimalist to-do list app. We can highlight a few key points:

  • Three internal states (tasks, isHandling and task) are required to manage the tasks list, the API request status and the input field value.

  • The internal states need to be updated at different times based on the UI requirements.

  • The API request status must be passed as a prop to the button to enable or disable it appropriately.

With useActionState and useFormStatus

typescript
// App.tsx
export const Todo = () => {
  const [tasks, handleSubmit, isHandling] = useActionState<string[], FormData>(
    async (previousState, newState) => {
      const response = await fetch(...);

      return [...previousState, response];
    },
    []
  );

  return (
    <form action={handleSubmit}>
      ...    
    <FormButton>Add</FormButton>
    </form>
  );
};

// FormButton.tsx
export const FormButton = ({ children }) => {
  const { pending } = useFormStatus();

  return (
    <button disabled={pending}>
      {pending ? "Please wait..." : children}
    </button>
  );
};

With the new hook, we no longer need to manage those three internal states, as the action will have an initial state and return everything we need to implement the UI. Additionally, we can use the new useFormStatus hook to capture the form status throughout the React tree without the need for prop drilling or using React context as you can see in the FormButton component. The hook itself will automatically capture the closest form status.

form actions

The new form actions help us manage internal form field states, meaning we no longer need to manage and map all form field values in an internal state. The action provides the FormData, from which we can retrieve the value of each field using the field’s name.

Without actions

typescript
// Controlled state
export const FormActions = () => {
  const [result, setResult] = useState<number>();
  
  const handleSubmit = async (e) => {
    e.preventDefault();

    const response = await fetch(...);

    setResult(response);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ... />
      <button>Save</button>
    </form>
  );
};

// Uncontrolled state
export const FormActions = () => {
  const [result, setResult] = useState<number>();
  const fieldRef = useRef<HTMLInputElement>(null);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
	  
    const value = fieldRef.current?.target.value || ""
    const response = await fetch(...);

    setResult(response);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input ref={fieldRef} ... />
      <button>Save</button>
    </form>
  );
};

The traditional way to handle forms is by mapping all fields to an internal state (either a single object or multiple primitives) and creating a submit callback function to handle the form submission. This means we need to retrieve all field values from internal states or refs before initiating the API request.

With actions

typescript
export const FormActions = () => {
  const [result, setResult] = useState<number>();
  const handleSubmit = async (data: FormData) => {
    const response = await fetch(...);
    setResult(response);
  };

  return (
    <form action={handleSubmit}>
      <input name="field_name" ... />
      <button>Save</button>
    </form>
  );
};

Using form actions, we can retrieve the form state through the FormData parameter, eliminating the need to map internal states to each individual form field. The useFormStatus hook also works perfectly with form actions. Now we no longer need to map form fields to internal states or refs because we can retrieve their values using the field names from the FormData object.

useOptimistic

The new useOptimistic hook allows us to apply updates optimistically while an async request is still in progress. We don’t need to wait for the API response to update the UI if we’re confident the request will succeed based on the client state, because we assume it succeeds. Otherwise, the UI changes will need to be reverted.

Without useOptimistic

typescript
const Todo = ({ tasks, onAdded }) => {
  const handleSubmit = async (formData: FormData) => {
    const newTask = formData.get("task")?.toString() || "";
    const response = await fetch(...);
    
    onAdded(response);
  };

  return (...);
};

export const UseOptimistic = () => {
  const [tasks, setTasks] = useState<string[]>([]);
  return (
    <Todo
      onAdded={(newTask) => setTasks((oldState) => [...oldState, newTask])}
      tasks={tasks}
    />
    ...
  );
};

We need to wait for the API request to finish in order to update the UI. Therefore, we can’t delay updating the UI until after firing the API request and synchronising it later when we receive a response. The UI is updated synchronously.

With useOptimistic

typescript
const Todo = ({ tasks, onAdded }) => {
  const [optTasks, setOptTasks] = useOptimistic<string[], string>(
    tasks,
    (currentState, newState) => [...currentState, newState]
  );

  const handleSubmit = async (formData: FormData) => {
    const newTask = formData.get("task")?.toString() || "";
    setOptTasks(newTask);
    const response = await fetch(...);
    onAdded(response);
  };

  // optTasks gets updated before the API request is sent.
  return (...);
};

export const UseOptimistic = () => {
  const [tasks, setTasks] = useState<string[]>([]);
  return (
    <Todo
      onAdded={(newTask) => setTasks((oldState) => [...oldState, newTask])}
      tasks={tasks}
    />
  );
};

Now the Todo component can update its internal state optimistically without waiting for the API response. Once the response is received, the parent component can synchronise the internal state with the updated API state. All of this happens behind the scenes, so the user remains unaware of the optimistic state being synchronised with the API.

use

The new use hook allows us to call hooks conditionally to resolve a Promise or read a value from any Context, for example. The hook itself can only be called inside a component, just like the old hooks.

use also supports Suspense by default.

Without use

typescript
const { data }  = useQuery({ ... });

if (!show) {
  return null;
}

return <p>API response: {data}</p>;

We can’t use hooks conditionally; all hooks must be called unconditionally, before any conditional rendering. This means the API request will be triggered even if it’s not needed.

With use

typescript
if (!show) {
  return null;
}

const response = use(fetch(...));

return <p>API response: {response}</p>;

By using the new use hook, we can conditionally render before calling the API. This means we will only trigger the necessary API requests.

ref as a prop

In React 19, we can consume references as a regular prop instead of using the forwardRef higher-order function.

Without ref as a prop

typescript
const MyInput = React.forwardRef(function MyInput(props, ref) {
  return <input {...props} ref={ref} />
})

//...
<MyInput placeholder="React v19" ref={ref} />

Using forwardRef, we can forward the ref to the correct internal element.

With ref as a prop

typescript
function MyInput({placeholder, ref}) {
  return <input placeholder={placeholder} ref={ref} />
}

//...
<MyInput placeholder="React v19" ref={ref} />

We can retrieve the ref using regular props, as usual. forwardRef will be deprecated in future releases.

<Context> as a provider

In React v19, we can render a <Context> as a provider instead of using the explicit <Context.Provider> property.

Without context as a provider

typescript
const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext.Provider>
      {children}
    </ThemeContext.Provider>
  );  
}

With context as a provider

typescript
const ThemeContext = createContext('');

function App({children}) {
  return (
    <ThemeContext>
      {children}
    </ThemeContext>
  );  
}

useDeferredValue initial value

Now, the useDeferredValue hook can have an initial value in its declaration.

Without useDeferredValue initial value

typescript
const [query, setQuery] = useState<string>('');
const value = useDeferredValue(query);

As you can see, we need to create an internal state (query) with an initial value and pass it to the useDeferredValue hook.

With useDeferredValue initial value

typescript
const value = useDeferredValue(deferredValue, '');

We no longer need to create the internal state because we can provide an initial value as the second parameter to the useDeferredValue hook.

Support for document metadata

Now we can provide metadata tags like <title>, <link> and <meta> within the React component tree to replace the high-level ones. As a result, we no longer need to use third-party libraries like react-helmet or write manual functions to manage them.

Without document metadata

typescript
const BlogPost = ({ post }) => (
  <Helmet>
    <title>{post.title}</title>
    <meta name="author" content={post.author.name} />
    <meta name="keywords" content={post.keywords} />
    <link rel="author" href={`https://twitter.com/${post.author.handle}/`} />
  </Helmet>
)

With document metadata

typescript
const BlogPost = ({ post }) => (
  <title>{post.title}</title>
  <meta name="author" content={post.author.name} />
  <meta name="keywords" content={post.keywords} />
  <link rel="author" href={`https://twitter.com/${post.author.handle}/`} />
)

Conclusion

Using the new React v19 features can make basic tasks easier to achieve without writing as much code as in previous versions.

Apart from the new hooks, React v19 also introduced new features. You can check the official blog post here.

Insight, imagination and expertly engineered solutions to accelerate and sustain progress.

Contact