DEV Community

Barry Michael Doyle
Barry Michael Doyle

Posted on • Edited on • Originally published at barrymichaeldoyle.com

Exploring the New useOptimistic Hook in React: Enhancing UI with Optimistic Updates

If you're as excited about React's new features as I am, you've probably been keeping an eye on the experimental builds. In this post, we're going to take an early look at an intriguing new feature that's not yet part of the official release: the useOptimistic hook. This hook promises to simplify the way we handle optimistic updates in our React applications, making them feel snappier and more responsive. Let's explore how to use this experimental feature and consider the potential it has to enhance our user experience.

Experimental Disclaimer

Please note, at the time of writing, React 18.2.0 is the latest stable release, and the useOptimistic hook we're about to explore hasn't been officially released. The functionality is experimental, and there may be changes before its final release.

To experiment with the useOptimistic hook, you'll need to install the experimental builds of react and react-dom. You can do this by running:

npm install react@experimental react-dom@experimental
Enter fullscreen mode Exit fullscreen mode

And then import it like this:

import { experimental_useOptimistic as useOptimistic } from 'react';
Enter fullscreen mode Exit fullscreen mode

If you're using TypeScript, remember to include "react/experimental" in your tsconfig.json file to ensure proper type recognition:

{
  "compilerOptions": {
    "types" ["react/experimental"]
  }
}
Enter fullscreen mode Exit fullscreen mode

Understanding Optimistic Updates

Optimistic updates aren't a new concept in web development, but they play a crucial role in enhancing user experience. This technique involves immediately updating the UI with expected changes, assuming that the corresponding server request will succeed. This creates the perception of a faster, more responsive application.

Here's how it typically works:

Imagine you have a list of todo items fetched from a server. When a user adds a new item, it requires a round trip to the server - which takes time. To make the UI feel faster, we can optimistically add the item to the list, making it appear as though the server has already confirmed the action. This is great in a perfect world, but we know network requests aren't always reliable. Handling potential errors and syncing state can become complex, which is where useOptimistic comes in, simplifying this process for React developers.

useOptimistic in Action

This example could be used in both an NextJS SSR app and a traditional client-side React application.

Imagine we had a TodoList component that contains a list of todo items and an input with a button to create a new todo item that gets added to the list.

Assume the TodoList component has a todos prop which is provided by data fetched from a server. We implement useOptimistic to optimistically update the UI as follows:

import { experimental_useOptimistic as useOptimistic } from 'react'
import { v4 as uuid } from 'uuid'
import { createTodo } from './actions'

type TodoItem = {
  id: string;
  item: string;
}

export function TodoList({ todos }: { todos: TodoItem[] }) {
  const [optimisticTodos, addOptimisticTodoItem] = useOptimistic<TodoItem[]>(
    todos,
    (state: TodoItem[], newTodoItem: string) => [
      ...state,
      { id: uuid(), item: newTodoItem },
    ]
  )

  return (
    <div>
      {optimisticTodos.map((todo) => (
        <div key={todo.id}>{todo.item}</div>
      ))}
      <form
        action={async (formData: FormData) => {
          const item = formData.get('item')
          addOptimisticTodoItem(item)
          await createTodo(item)
        }}
      >
        <input type="text" name="item" />
        <button type="submit">Add Item</button>
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Breaking down the useOptimistic hook

Let's look at the hook on its own with placeholder elements:

const [A, B] = useOptimistic<T>(C, D)
Enter fullscreen mode Exit fullscreen mode
  • A is the optimistic state, it will default to what C is.
  • B is the dispatching function to call that will run what we define in D.
  • C is the source of truth, if C is ever changed i.e. we get a new value in from the server, A will be set the that too since it will always treat C as the final source of truth.
  • D is the mutation that will occur to A when B is called.
  • T is an optional property for TypeScript users to define the type for the source of truth.

Additional Optimistic Properties

You can further leverage the useOptimistic hook by including additional properties in the mutation.

For example, let's say we want a way to indicate that an update is optimistic to the user. We can do so by adding a pending: true property to the optimistic update and render the todo item a gray color until the update has properly occurred on the server.

We can do that by updating our initial example to this:

export function TodoList({ todos }: { todos: TodoItem[] }) {
  const [optimisticTodos, addOptimisticTodoItem] = useOptimistic<TodoItem[]>(
    todos,
    (state: TodoItem[], newTodoItem: string) => [
      ...state,
      { item: newTodoItem, pending: true },
    ]
  )

  return (
    <div>
      {optimisticTodos.map((todo) => (
        <div
          key={todo.id}
          style={{ color: todo.pending ? "gray" : "inherit" }}
        >
          {todo.item}
        </div>
      ))}
      <form
        action={async (formData: FormData) => {
          const item = formData.get('item')
          addOptimisticTodoItem(item)
          await createTodo(item)
        }}
      >
        <input type="text" name="item" />
        <button type="submit">Add Item</button>
      </form>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now when we submit a new todo item, optimistically our UI will update to the initial todo list plus one new todo item with the pending property as true. In our render method the pending todo item will be styled to have a gray color. Once the server update has occurred, the todos prop - which is the source of truth - would have changed which will cause the optimisticTodos value to update to the new source of truth. This new source of truth will include the optimistically updated value without the pending property, so the new item will no longer have a gray color.

Conclusion

While still experimental, the useOptimistic hook offers an exciting glimpse into the future of state management in React applications. It aims to simplify the implementation of optimistic UI updates, contributing to faster, more responsive user experiences. This feature seems particularly promising when combined with NextJS's SSR capabilities, though it remains experimental at this stage.

As the React community anticipates the official release of this feature, I'm interested to hear your thoughts. Have you tried the useOptimistic hook in your projects? What potential do you see for this feature in real-world applications? Share your experiences and insights in the comments below!

Top comments (12)

Collapse
 
dumebii profile image
Dumebi Okolo

Wow. Thanks for sharing!

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

I’m glad you found this interesting! Hopefully this prepares you for when it is properly ready! 🥳

Collapse
 
dumebii profile image
Dumebi Okolo

Yesss. Thank you.

Collapse
 
ariannargesi profile image
Arian Nargesi

Thank you @barrymichaeldoyle for this great explanation.
How do we handle errors? What to do when network request fails? Is there any mechanism to undo the optimistic update?

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

Sorry I've taken so long to get back to you.

The way the useOptimistic hook works is that the optimistic update resets when the the asynchronous form action is completed and the actual rendered value gets replaced by the new todos. It's not really shown in my example code in this post.

So in the case of an error - which will be handled in createTodo - the optimistic value will be cleared when the promise rejects because todos doesn't change. The way you decide to handle an error specifically is up to you e.g. you could render a toast, or show an error message under the todos list, up to you.

Collapse
 
mandiaroux profile image
Mandia Roux

Looks awesome, thanks for the clear and consise explanation 😀

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

It's such a pleasure and thank you for the encouraging feedback!

Collapse
 
jc005789 profile image
jayden

Why useOptimistic is undefined in my app after I installed like so

npm install react@experimental react-dom@experimental

I tried import { useOptimistic } from 'react' as well but does not work.

It keeps erroring out saying 0__.useOptimistic) is not a function or its return value is not iterable

Collapse
 
ahmetkca profile image
ahmetkca

What if you mutate your data somewhere else? How would you use useOptimistic hook then? Perhaps, with state management libraries such as zustand?

Collapse
 
ashukumar2001 profile image
Ashu Kumar

You can wrap your action into a transition
`const [isPending, startTransition] = useTransition();

const onUpdate = () => {
startTransition(()=> {
optimisticUpdate();
})
}

`

Collapse
 
barrymichaeldoyle profile image
Barry Michael Doyle

In that scenario I'd recommend going with something like zustand yes.

Collapse
 
syedmuhammadaliraza profile image
Syed Muhammad Ali Raza

Thanks for sharing!