DEV Community

Bolaji Bolajoko
Bolaji Bolajoko

Posted on

React useOptimistic: Harnessing the power of snappy UIs!

Am I the only one who finds useOptimistic both fascinating and amazing? If you're curious and want to dive deep into how to use this hook, you're in the right place.

đź’ˇ useOptimistic hook, is someone who always starts a band, forever hopeful that the next hit will be a success.

The useOptimistic hook is a React hook that lets you show a different state when an asynchronous action is processing. If the action fails, the state can be reverted to its previous state. I like to think of it as a client that knows more than the server. Instead of waiting for a response from the server, we can use the optimistic state to update the UI while the asynchronous action is underway. This makes the app feel faster and more responsive to the user.

useOptimistic is only available on React 19. The example in this writing make use of Nextjs 14.

Scenario Example: Imagine a comment section where you can optimistically update the comment list to show the latest comment. We can immediately display the new comment instead of waiting for the roundtrip to the server. If the server request fails, the previous state is restored, and possibly an indicator is shown to the user that the comment update has failed.

Always implement a rollback mechanism and provide user feedback if the asynchronous operation fails. This enhances the user experience.

useOptimistic(state, updateFn)

The useOptimistic takes in two parameters state, updateFn.

Parameters:

  • initialState: This is the initial state.
  • updateFn: A function used to apply the optimistic update to the state.

Returns:

  • optimisticState: The optimistic state returned by updateFn.
  • addOptimistic: A function used to update the optimistic state.

More Insights on Parameters and Returned Values:

To understand how to use useOptimistic, we need to explore how the parameters and return values fit together.

Parameters:

  1. initialState
    • Type: It can be of any type (string, number, array, boolean, etc.).
    • Purpose: This is the initial state that your component starts with. It is the baseline before any optimistic update is applied.
    • Example: If you're managing a list of comments, initialState can be an array fetched from your database.
const commentList = [{ id: 1, comment: "This is a comment" }, {id: 2, comment: "This is another comment" }]
Enter fullscreen mode Exit fullscreen mode
  1. updateFn
  2. Type: (currentState: StateType, newOptimisticValue: any) => StateType
  3. Purpose: This function defines how the state should update. It receives the current state and the value you want to optimistically update (e.g., a new comment) and returns the updated state.
const updateFn = (currentState, newValues) => [...currentState, newValues]
Enter fullscreen mode Exit fullscreen mode

This function should be pure, meaning it should not cause any side effects and should return the same state based on the input.

Return Values:

  1. optimisticState
    • Type: Same as initialState.
    • Purpose: This represents the current state of the component after the optimistic update has been applied. As you apply an optimistic update using the addOptimistic function, optimisticState reflects the changes made.
    • Example
const [optimisticState, addOptimistic] = useOptimistic(commentList, (currentState, newComment) => [...currentState, newComment])

return (
  <ul>
    {optimisticState.map((comment) => (
      <li key={comment.id}>{comment.text}</li>
    ))}
  </ul>
);
Enter fullscreen mode Exit fullscreen mode

addOptimistic

  • Type: (newOptimisticValue: any) => void
  • Purpose: This function is used to apply an optimistic update to the state. When you call this function, it triggers updateFn with the current state and the new optimistic value, returning the new state.
  • Example
const newComment = { id: 3, comment: "Add new comment" }
addOptimistic(newComment);
Enter fullscreen mode Exit fullscreen mode

That was a long overview, but I hope you learned something about how the pieces fit together. If not, feel free to go over it again. Having a solid foundation will provide a blueprint for how to use this hook effectively.

Optimistic Shopping List

We'll create a shopping list that adds items to the cart optimistically. We'll use JavaScript Promises and setTimeout to simulate a network request and a custom function to rollback the previous data if the request fails.

// here is our initial data and type
type Item = {
  id: number;
  name: string;
};
const initialState: Item[] = [
  { id: 1, name: "Apple" },
  { id: 2, name: "Orange" },
  { id: 3, name: "Cherry" },
  { id: 4, name: "Pineapple" },
  { id: 5, name: "Grape" },
];
Enter fullscreen mode Exit fullscreen mode

Since we don’t have a server setup, we’ll simulate a server response using JavaScript Promise and setTimeout.


// Server simulation function
// NOTE: Do not put this function into a component
// to avoid re-creation on each render

async function simulateServer(cartItem: Item[], item: Item) {
  return new Promise<Item[]>((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.3) {
        resolve([...cartItem, item]);
      } else {
        {
          reject(new Error("Failed"));
        }
      }
    }, 3000);
  });
}
Enter fullscreen mode Exit fullscreen mode

In our simulation, if the Math.random() value is greater than 0.3, the function simulates an OK response and adds new items to the cart. Otherwise, the response fails by calling the reject function.

// here is the shoppingList component
export default function ShoppingList() {
  const [cartItem, setCartItem] = useState<Item[]>([]);
  const [error, setError] = useState<string>("");
  const [optimisticState, addOptimistic] = useOptimistic<Item[], Item>(
    cartItem,
    (currentState, newValue) => [...currentState, newValue]
  );

  async function addNewItem(item: Item) {
    startTransition(() => { // `startTransition` for non-urgent updates that shouldn't block the user interface.
      addOptimistic(item);
    });

    try {
      const newItem = await simulateServer(optimisticState, item);
      setCartItem(newItem);
    } catch (error) {
      setError(`Failed to add item ${item.name}`);
      rollbackOptimisticUpdate(item.id);
    }
  }

  useEffect(() => {
    if (error) {
      const timer = setTimeout(() => setError(""), 3000);
      return () => clearTimeout(timer);
    }
  }, [error]);

    // rollback the previous data if request failed
  function rollbackOptimisticUpdate(tempId: number) {
    startTransition(() => {
      const revertItem = cartItem.filter((item) => item.id !== tempId)
      setCartItem(revertItem);

      revertItem.forEach(item => addOptimistic(item))
    });
  }

  return (
    <>
      <div className="flex gap-20 justify-between">
        <ul>
          {initialState.map((item: Item) => {
            return (
              <li
                key={item.id}
                className="flex gap-4 justify-between  font-semibold p-4 border border-slate-100 "
              >
                {item.name}
                <button
                  onClick={() => addNewItem(item)}
                  className="px-3 py-2 rounded-md hover:bg-slate-400 bg-slate-100 text-black"
                >
                  Add
                </button>
              </li>
            );
          })}
        </ul>
        <div className="relative">
          <FaCartShopping size={100} />
          <div className="absolute top-4 inset-0 ">
            <p className="text-3xl text-slate-800 text-center font-bold">
              {optimisticState.length}
            </p>
          </div>
        </div>
        <p className=" text-red-300 font-semibold">{error}</p>
      </div>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • addNewItem function handles the addition of items to the cart.
    • It triggers an optimistic update using addOptimistic, adding the new item to optimisticState without waiting for a server response.
    • It then attempts to upadte the server by calling simulateServer. If the server responds successfully, the cartItem state is updated with the confirmed data.
    • If the server request fails, the error is captured, and the rollbackOptimisticUpdate function is called to remove the optimistic update.
  • The rollbackOptimisticUpdate function uses addOptimistic to filter the item that failed to update, effectively rolling back the optimistic state to what it was before the failed operation.

Conclusion

Optimistic way of rendering data, coupled with rollback mechanism, ensures that users get a responsive feel like interaction and quick feedback if any error occurs, while maintaining a smooth interaction flow. Leveraging useOptimistic can significantly enhance the perceived performance of your React application.

Top comments (0)