useOptimistic

useOptimistic is a React Hook that lets you optimistically update the UI.

const [optimisticState, setOptimistic] = useOptimistic(value, reducer?);

Reference

useOptimistic(value, reducer?)

Call useOptimistic at the top level of your component to create optimistic state for a value.

import { useOptimistic } from 'react';

function MyComponent({name, todos}) {
const [optimisticAge, setOptimisticAge] = useOptimistic(28);
const [optimisticName, setOptimisticName] = useOptimistic(name);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, todoReducer);
// ...
}

See more examples below.

Parameters

  • value: The value returned when there are no pending Actions.
  • optional reducer(currentState, action): The reducer function that specifies how the optimistic state gets updated. It must be pure, should take the current state and reducer action arguments, and should return the next optimistic state.

Returns

useOptimistic returns an array with exactly two values:

  1. optimisticState: The current optimistic state. It is equal to value unless an Action is pending, in which case it is equal to the state returned by reducer (or the value passed to the set function if no reducer was provided).
  2. The set function that lets you update the optimistic state to a different value inside an Action.

set functions, like setOptimistic(optimisticState)

The set function returned by useOptimistic lets you update the state for the duration of an Action. You can pass the next state directly, or a function that calculates it from the previous state:

const [optimisticLike, setOptimisticLike] = useOptimistic(false);
const [optimisticSubs, setOptimisticSubs] = useOptimistic(subs);

function handleClick() {
startTransition(async () => {
setOptimisticLike(true);
setOptimisticSubs(a => a + 1);
await saveChanges();
});
}

Parameters

  • optimisticState: The value that you want the optimistic state to be during an Action. If you provided a reducer to useOptimistic, this value will be passed as the second argument to your reducer. It can be a value of any type.
    • If you pass a function as optimisticState, it will be treated as an updater function. It must be pure, should take the pending state as its only argument, and should return the next optimistic state. React will put your updater function in a queue and re-render your component. During the next render, React will calculate the next state by applying the queued updaters to the previous state similar to useState updaters.

Returns

set functions do not have a return value.

Caveats

  • The set function must be called inside an Action. If you call the setter outside an Action, React will show a warning and the optimistic state will briefly render.
Deep Dive

How optimistic state works

useOptimistic lets you show a temporary value while a Action is in progress:

const [value, setValue] = useState('a');
const [optimistic, setOptimistic] = useOptimistic(value);

startTransition(async () => {
setOptimistic('b');
const newValue = await saveChanges('b');
setValue(newValue);
});

When the setter is called inside an Action, useOptimistic will trigger a re-render to show that state while the Action is in progress. Otherwise, the value passed to useOptimistic is returned.

This state is called the “optimistic” because it is used to immediately present the user with the result of performing an Action, even though the Action actually takes time to complete.

How the update flows

  1. Update immediately: When setOptimistic('b') is called, React immediately renders with 'b'.

  2. (Optional) await in Action: If you await in the Action, React continues showing 'b'.

  3. Transition scheduled: setValue(newValue) schedules an update to the real state.

  4. (Optional) wait for Suspense: If newValue suspends, React continues showing 'b'.

  5. Single render commit: Finally, the newValue is commits for value and optimistic.

There’s no extra render to “clear” the optimistic state. The optimistic and real state converge in the same render when the Transition completes.

Note

Optimistic state is temporary

Optimistic state is only renders while an Action is in progress, otherwise value is rendered.

If saveChanges returned 'c', then both value and optimistic will be 'c', not 'b'.

How the final state is determined

The value argument to useOptimistic determines what displays after the Action finishes. How this works depends on the pattern you use:

  • Hardcoded values like useOptimistic(false): After the Action, state is still false, so the UI shows false. This is useful for pending states where you always start from false.

  • Props or state passed in like useOptimistic(isLiked): If the parent updates isLiked during the Action, the new value is used after the Action completes. This is how the UI reflects the result of the Action.

  • Reducer pattern like useOptimistic(items, fn): If items changes while the Action is pending, React re-runs your reducer with the new items to recalculate the state. This keeps your optimistic additions on top of the latest data.

What happens when the Action fails

If the Action throws an error, the Transition still ends, and React renders with whatever value currently is. Since the parent typically only updates value on success, a failure means value hasn’t changed, so the UI shows what it showed before the optimistic update. You can catch the error to show a message to the user.


Usage

Adding optimistic state to a component

Call useOptimistic at the top level of your component to declare one or more optimistic states.

import { useOptimistic } from 'react';

function MyComponent({age, name, todos}) {
const [optimisticAge, setOptimisticAge] = useOptimistic(age);
const [optimisticName, setOptimisticName] = useOptimistic(name);
const [optimisticTodos, setOptimisticTodos] = useOptimistic(todos, reducer);
// ...

useOptimistic returns an array with exactly two items:

  1. The optimistic state, initially set to the value provided.
  2. The set function that lets you temporarily change the state during an Action.
    • If a reducer is provided, it will run before returning the optimistic state.

To use the optimistic state, call the set function inside an Action.

Actions are functions called inside startTransition:

function onAgeChange(e) {
startTransition(async () => {
setOptimisticAge(42);
const newAge = await postAge(42);
setAge(newAge);
});
}

React will render the optimistic state 42 first while the age remains the current age. The Action waits for POST, and then renders the newAge for both age and optimisticAge.

See How optimistic state works for a deep dive.

Note

When using Action props, you can call the set function without startTransition:

async function submitAction() {
setOptimisticName('Taylor');
await updateName('Taylor');
}

This works because Action props are already called inside startTransition.

For an example, see: Using optimistic state in Action props.


Using optimistic state in Action props

In an Action prop, you can call the optimistic setter directly without startTransition.

This example sets optimistic state inside a <form> submitAction prop:

import { useOptimistic, startTransition } from 'react';
import { updateName } from './actions.js';

export default function EditName({ name, action }) {
  const [optimisticName, setOptimisticName] = useOptimistic(name);

  async function submitAction(formData) {
    const newName = formData.get('name');
    setOptimisticName(newName);
    
    const updatedName = await updateName(newName);
    startTransition(() => {
      action(updatedName);
    })
  }

  return (
    <form action={submitAction}>
      <p>Your name is: {optimisticName}</p>
      <p>
        <label>Change it: </label>
        <input
          type="text"
          name="name"
          disabled={name !== optimisticName}
        />
      </p>
    </form>
  );
}

In this example, when the user submits the form, the optimisticName updates immediately to show the newName optimistically while the server request is in progress. When the request completes, name and optimisticName are rendered with the actual updatedName from the response.

Deep Dive

Why doesn’t this need startTransition?

By convention, props called inside startTransition are named with “Action”.

Since submitAction is named with “Action”, you know it’s already called inside startTransition.

See Exposing action prop from components for the Action prop pattern.


Adding optimistic state to Action props

When creating an Action prop, you can add useOptimistic to show immediate feedback.

Here’s a button that shows “Submitting…” while the action is pending:

import { useOptimistic, startTransition } from 'react';

export default function Button({ action, children }) {
  const [isPending, setIsPending] = useOptimistic(false);

  return (
    <button
      disabled={isPending}
      onClick={() => {
        startTransition(async () => {
          setIsPending(true);
          await action();
        });
      }}
    >
      {isPending ? 'Submitting...' : children}
    </button>
  );
}

When the button is clicked, setIsPending(true) uses optimistic state to immediately show “Submitting…” and disable the button. When the Action is done, isPending is rendered as false automatically.

This pattern automatically shows a pending state however action prop is used with Button:

// Show pending state for a state update
<Button action={() => { setState(c => c + 1) }} />

// Show pending state for a navigation
<Button action={() => { navigate('/done') }} />

// Show pending state for a POST
<Button action={async () => { await fetch(/* ... */) }} />

// Show pending state for any combination
<Button action={async () => {
setState(c => c + 1);
await fetch(/* ... */);
navigate('/done');
}} />

The pending state will be shown until everything in the action prop is finished.

Note

You can also use useTransition to get pending state via isPending.

The difference is that useTransition gives you the startTransition function, while useOptimistic works with any Transition. Use whichever fits your component’s needs.


Updating props or state optimistically

You can wrap props or state in useOptimistic to update it immediately while an Action is in progress.

In this example, LikeButton receives isLiked as a prop and immediately toggles it when clicked:

import { useState, useOptimistic, startTransition } from 'react';
import { toggleLike } from './actions.js';

export default function App() {
  const [isLiked, setIsLiked] = useState(false);
  const [optimisticIsLiked, setOptimisticIsLiked] = useOptimistic(isLiked);

  function handleClick() {
    startTransition(async () => {
      const newValue = !optimisticIsLiked
      console.log('⏳ setting optimistic state: ' + newValue);
      
      setOptimisticIsLiked(newValue);
      const updatedValue = await toggleLike(newValue);
      
      startTransition(() => {
        console.log('⏳ setting real state: ' + updatedValue );
        setIsLiked(updatedValue);
      });
    });
  }

  if (optimisticIsLiked !== isLiked) {
    console.log('✅ rendering optmistic state: ' + optimisticIsLiked);  
  } else {
    console.log('✅ rendering real value: ' + optimisticIsLiked);
  }
  

  return (
    <button onClick={handleClick}>
      {optimisticIsLiked ? '❤️ Unlike' : '🤍 Like'}
    </button>
  );
}

When the button is clicked, setOptimisticIsLiked immediately updates the displayed state to show the heart as liked. Meanwhile, await toggleLike runs in the background. When the await completes, setIsLiked parent updates the “real” isLiked state, and the optimistic state is rendered to match this new value.

Note

This example reads from optimisticIsLiked to calculate the next value. This works when the base state won’t change, but if the base state might change while your Action is pending, you may want to use a state updater or the reducer.

See Updating state based on the current state for an example.


Updating multiple values together

When an optimistic update affects multiple related values, use a reducer to update them together. This ensures the UI stays consistent.

Here’s a follow button that updates both the follow state and follower count:

import { useOptimistic, startTransition } from 'react';

export default function FollowButton({ user, followAction }) {
  const [optimisticState, updateOptimistic] = useOptimistic(
    { isFollowing: user.isFollowing, followerCount: user.followerCount },
    (current, isFollowing) => ({
      isFollowing,
      followerCount: current.followerCount + (isFollowing ? 1 : -1)
    })
  );

  function handleClick() {
    const newFollowState = !optimisticState.isFollowing;
    startTransition(async () => {
      updateOptimistic(newFollowState);
      await followAction(newFollowState);
    });
  }

  return (
    <div>
      <p><strong>{user.name}</strong></p>
      <p>{optimisticState.followerCount} followers</p>
      <button onClick={handleClick}>
        {optimisticState.isFollowing ? 'Unfollow' : 'Follow'}
      </button>
    </div>
  );
}

The reducer receives the new isFollowing value and calculates both the new follow state and the updated follower count in a single update. This ensures the button text and count always stay in sync.

Deep Dive

Choosing between updaters and reducers

useOptimistic supports two patterns for calculating state based on current state:

Updater functions work like useState updaters. Pass a function to the setter:

const [optimistic, setOptimistic] = useOptimistic(value);
setOptimistic(current => !current);

Reducers separate the update logic from the setter call:

const [optimistic, dispatch] = useOptimistic(value, (current, action) => {
// Calculate next state based on current and action
});
dispatch(action);

Use updaters for calculations where the setter call naturally describes the update. This is similar to using setState(prev => ...) with useState.

Use reducers when you need to pass data to the update (like which item to add) or when handling multiple types of updates with a single hook.

Why use a reducer?

Reducers are essential when the base state might change while your Transition is pending. If todos changes while your add is pending (for example, another user added a todo), React will re-run your reducer with the new todos to recalculate what to show. This ensures your new todo is added to the latest list, not an outdated copy.

An updater function like setOptimistic(prev => [...prev, newItem]) would only see the state from when the Transition started, missing any updates that happened during the async work.


Optimistically adding to a list

When you need to optimistically add items to a list, use a reducer:

import { useOptimistic, startTransition } from 'react';

export default function TodoList({ todos, addTodoAction }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (currentTodos, newTodo) => [
      ...currentTodos,
      { id: newTodo.id, text: newTodo.text, pending: true }
    ]
  );

  function handleAddTodo(text) {
    const newTodo = { id: crypto.randomUUID(), text: text };
    startTransition(async () => {
      addOptimisticTodo(newTodo);
      await addTodoAction(newTodo);
    });
  }

  return (
    <div>
      <button onClick={() => handleAddTodo('New todo')}>Add Todo</button>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id}>
            {todo.text} {todo.pending && "(Adding...)"}
          </li>
        ))}
      </ul>
    </div>
  );
}

The reducer receives the current list of todos and the new todo to add. This is important because if the todos prop changes while your add is pending (for example, another user added a todo), React will update your optimistic state by re-running the reducer with the updated list. This ensures your new todo is added to the latest list, not an outdated copy.

Note

Each optimistic item includes a pending: true flag so you can show loading state for individual items. When the server responds and the parent updates the canonical todos list with the saved item, the optimistic state updates to the confirmed item without the pending flag.


Handling multiple action types

When you need to handle multiple types of optimistic updates (like adding and removing items), use a reducer pattern with action objects.

This shopping cart example shows how to handle add and remove with a single reducer:

import { useOptimistic, startTransition } from 'react';

export default function ShoppingCart({ cart, cartActions }) {
  const [optimisticCart, dispatch] = useOptimistic(
    cart,
    (currentCart, action) => {
      switch (action.type) {
        case 'add':
          const exists = currentCart.find(item => item.id === action.item.id);
          if (exists) {
            return currentCart.map(item =>
              item.id === action.item.id
                ? { ...item, quantity: item.quantity + 1, pending: true }
                : item
            );
          }
          return [...currentCart, { ...action.item, quantity: 1, pending: true }];
        case 'remove':
          return currentCart.filter(item => item.id !== action.id);
        case 'update_quantity':
          return currentCart.map(item =>
            item.id === action.id
              ? { ...item, quantity: action.quantity, pending: true }
              : item
          );
        default:
          return currentCart;
      }
    }
  );

  function handleAdd(item) {
    startTransition(async () => {
      dispatch({ type: 'add', item });
      await cartActions.add(item);
    });
  }

  function handleRemove(id) {
    startTransition(async () => {
      dispatch({ type: 'remove', id });
      await cartActions.remove(id);
    });
  }

  function handleUpdateQuantity(id, quantity) {
    startTransition(async () => {
      dispatch({ type: 'update_quantity', id, quantity });
      await cartActions.updateQuantity(id, quantity);
    });
  }

  const total = optimisticCart.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );

  return (
    <div>
      <h2>Shopping Cart</h2>
      <div style={{ marginBottom: 16 }}>
        <button onClick={() => handleAdd({
          id: 1, name: 'T-Shirt', price: 25
        })}>
          Add T-Shirt ($25)
        </button>{' '}
        <button onClick={() => handleAdd({
          id: 2, name: 'Mug', price: 15
        })}>
          Add Mug ($15)
        </button>
      </div>
      {optimisticCart.length === 0 ? (
        <p>Your cart is empty</p>
      ) : (
        <ul>
          {optimisticCart.map(item => (
            <li key={item.id}>
              {item.name} - ${item.price} ×
              {item.quantity}
              {' '}= ${item.price * item.quantity}
              <button
                onClick={() => handleRemove(item.id)}
                style={{ marginLeft: 8 }}
              >
                Remove
              </button>
              {item.pending && ' ...'}
            </li>
          ))}
        </ul>
      )}
      <p><strong>Total: ${total}</strong></p>
    </div>
  );
}

The reducer handles three action types (add, remove, update_quantity) and returns the new optimistic state for each. Each action sets a pending: true flag so you can show visual feedback while the Server Function runs.


Optimistic delete with error recovery

When deleting items optimistically, you should handle the case where the Action fails.

This example shows how to display an error message when a delete fails, and the UI automatically rolls back to show the item again.

import { useState, useOptimistic, startTransition } from 'react';

export default function ItemList({ items, deleteAction }) {
  const [error, setError] = useState(null);
  const [optimisticItems, removeItem] = useOptimistic(
    items,
    (currentItems, idToRemove) =>
      currentItems.map(item =>
        item.id === idToRemove
          ? { ...item, deleting: true }
          : item
      )
  );

  function handleDelete(id) {
    setError(null);
    startTransition(async () => {
      removeItem(id);
      try {
        await deleteAction(id);
      } catch (e) {
        setError(e.message);
      }
    });
  }

  return (
    <div>
      <h2>Your Items</h2>
      <ul>
        {optimisticItems.map(item => (
          <li
            key={item.id}
            style={{
              opacity: item.deleting ? 0.5 : 1,
              textDecoration: item.deleting ? 'line-through' : 'none',
              transition: 'opacity 0.2s'
            }}
          >
            {item.name}
            <button
              onClick={() => handleDelete(item.id)}
              disabled={item.deleting}
              style={{ marginLeft: 8 }}
            >
              {item.deleting ? 'Deleting...' : 'Delete'}
            </button>
          </li>
        ))}
      </ul>
      {error && (
        <p style={{ color: 'red', padding: 8, background: '#fee' }}>
          {error}
        </p>
      )}
    </div>
  );
}

Try deleting ‘Deploy to production’. When the delete fails, the item automatically reappears in the list.


Troubleshooting

I’m getting an error: “An optimistic state update occurred outside a Transition or Action”

You may see this error:

Console
An optimistic state update occurred outside a Transition or Action. To fix, move the update to an Action, or wrap with startTransition.

The optimistic setter function must be called inside startTransition:

// 🚩 Incorrect: outside a Transition
function handleClick() {
setOptimistic(newValue); // Warning!
// ...
}

// ✅ Correct: inside a Transition
function handleClick() {
startTransition(async () => {
setOptimistic(newValue);
// ...
});
}

// ✅ Also correct: inside an Action prop
function submitAction(formData) {
setOptimistic(newValue);
// ...
}

When you call the setter outside an Action, the optimistic state will briefly appear and then immediately revert back to the original value. This happens because there’s no Transition to “hold” the optimistic state while your Action runs.

I’m getting an error: “Cannot update optimistic state while rendering”

You may see this error:

Console
Cannot update optimistic state while rendering.

This error occurs when you call the optimistic setter during the render phase of a component. You can only call it from event handlers, effects, or other callbacks:

// 🚩 Incorrect: calling during render
function MyComponent({ items }) {
const [isPending, setPending] = useOptimistic(false);

// This runs during render - not allowed!
setPending(true);

// ...
}

// ✅ Correct: calling inside startTransition
function MyComponent({ items }) {
const [isPending, setPending] = useOptimistic(false);

function handleClick() {
startTransition(() => {
setPending(true);
// ...
});
}

// ...
}

// ✅ Also correct: calling from an Action
function MyComponent({ items }) {
const [isPending, setPending] = useOptimistic(false);

function action() {
setPending(true);
// ...
}

// ...
}

My optimistic updates show stale values

If your optimistic state seems to be based on old data, consider using an updater function or reducer to calculate the optimistic state relative to the current state.

// May show stale data if state changes during Action
const [optimistic, setOptimistic] = useOptimistic(count);
setOptimistic(5); // Always sets to 5, even if count changed

// Better: relative updates handle state changes correctly
const [optimistic, adjust] = useOptimistic(count, (current, delta) => current + delta);
adjust(1); // Always adds 1 to whatever the current count is

See Updating state based on the current state for details.

I don’t know if my optimistic update is pending

To know when useOptimistic is pending, you have three options:

  1. Check if optimisticValue === value
const [optimistic, setOptimistic] = useOptimistic(value);
const isPending = optimistic !== value;

If the values are not equal, there’s a Transition in progress.

  1. Add a useTransition
const [isPending, startTransition] = useTransition();
const [optimistic, setOptimistic] = useOptimistic(value);

//...
startTransition(() => {
setOptimistic(state);
})

Since useTransition uses useOptimsitic for isPending under the hood, this is equivalent to option 1.

3Add a pending flag in your reducer

const [optimistic, addOptimistic] = useOptimistic(
items,
(state, newItem) => [...state, { ...newItem, isPending: true }]
);