AI Workshop: learn to build apps with AI →
Next.js (App Router): The useOptimistic hook

Join the AI Workshop and learn to build real-world apps with AI. A hands-on, practical program to level up your skills.


This hook lets you update the UI immediately in response to an action, before the server responds.

You pass it the current state you want to manage (for example, an array messages), and a function to update the optimistic state.

It gives you the optimistic state (which you use for immediate rendering), and a function to update it.

You call the hook before the server request.

After the server response is received, you update the actual state (in the example, messages). In the TSX you render the optimistic state, which then shows the actual state once you update it with the server response.

You can use this hook in a client component.

Here’s a simple todo app example.

Define an addTodo server action:

'use server'

type Todo = {
  todo: string
}

export async function addTodo(newTodo: string): Promise<Todo> {
  // Simulating server delay
  await new Promise((resolve) => setTimeout(resolve, 3000))
  // Add todo on server
  return {
    todo: newTodo + ' test',
  }
}

In this server action, after 3 seconds we return the todo sent, plus ' test' to understand that this is the returned value from the server action.

Client-side we use the useOptimistic hook to generate an optimisticTodos array, which we use to list todos in the TSX:


const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
  todos,
  (state, newTodo) => [...state, { todo: newTodo }]
)

When we hit the form action (the user presses the submit button), the addOptimisticTodo() function is called to add the new todo.

Then we hit the server, which takes some time as the server action waits 3 seconds.

Finally when we’re back we call setTodos() to update the todos list with the actual state coming from the server.

Full code:

'use client'

import { useOptimistic, useState, useRef } from 'react'
import { addTodo } from './actions'

type Todo = {
  todo: string
}

export default function Todos() {
  const [todos, setTodos] = useState<Todo[]>([])
  const formRef = useRef<HTMLFormElement>(null)

  const [optimisticTodos, addOptimisticTodo] = useOptimistic<Todo[], string>(
    todos,
    (state, newTodo) => [...state, { todo: newTodo }]
  )

  const formAction = async (formData: FormData) => {
    const todo = formData.get('todo') as string
    addOptimisticTodo(todo)
    formRef.current?.reset()

    try {
      const result = await addTodo(todo)

      // Update the actual state with the server response
      setTodos((prevTodos) => [...prevTodos, { todo: result.todo }])
    } catch (error) {
      console.error('Error adding todo:', error)
      // Optionally, you could remove the optimistic update here if the server request fails
    }
  }

  return (
    <div>
      {optimisticTodos.map((m, i) => (
        <div key={i}>{m.todo}</div>
      ))}
      <form action={formAction} ref={formRef}>
        <input type='text' name='todo' />
        <button type='submit'>Send</button>
      </form>
    </div>
  )
}

Lessons in this unit:

0: Introduction
1: Server Actions
2: Next.js, passing an id to a server action
3: The use hook
4: use() and data fetching
5: The useFormStatus Hook
6: ▶︎ The useOptimistic hook
7: The useActionState hook
8: How to get the Request headers in Next.js app router
9: How to install shadcn-ui on latest Next.js beta-RC