0Pricing
Next.js 15 Fullstack (App Router + Server Actions) · Leçon

Retour instantané avec useOptimistic

Appliquez des mises à jour optimistes de l’interface pour les mentions « J’aime », les commentaires et les boutons à bascule pendant que l’action s’exécute en arrière-plan.

Retour instantané avec useOptimistic est une leçon Next.js 15 Fullstack (App Router + Server Actions) gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Next.js 15 Fullstack (App Router + Server Actions), et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Certaines parties de cette leçon n'ont pas encore été traduites et s'affichent en anglais.

The Latency Problem

When a user clicks a like button, the change has to travel to the server and back before the UI reflects it. On a slow network that delay feels broken.

The fix is optimistic UI: assume the action will succeed, update the screen instantly, and reconcile with the server once it responds. If the server fails, you roll back automatically.

React 19 (shipped with Next.js 15) gives you a dedicated hook for this: useOptimistic.

What useOptimistic Returns

useOptimistic takes your real state and an update function, and returns a temporary optimistic value plus a function to apply optimistic changes.

  • optimisticState — what you render right now
  • addOptimistic(value) — queue an optimistic change

The signature is useOptimistic(state, (currentState, optimisticValue) => newState). The reducer-style function merges the optimistic value into the current state.

const [optimisticState, addOptimistic] = useOptimistic(
  state,
  (currentState, optimisticValue) => {
    // return the new state to display optimistically
    return { ...currentState, ...optimisticValue };
  }
);

A Like Button: The Server Action

First, the real work happens in a Server Action. It runs on the server, mutates the database, and revalidates the cache.

Notice the "use server" directive and the revalidatePath call so other parts of the app see the fresh count.

"use server";

import { revalidatePath } from "next/cache";
import { db } from "@/lib/db";

export async function toggleLike(postId: string, liked: boolean) {
  await db.post.update({
    where: { id: postId },
    data: { likes: { increment: liked ? 1 : -1 } },
  });
  revalidatePath("/feed");
}

Wiring useOptimistic to the Button

In a Client Component, we hold the optimistic like state. When clicked, we call addOptimistic first (instant UI), then await the Server Action.

If the action throws, React automatically discards the optimistic value and reverts to the real likes prop.

"use client";

import { useOptimistic, useTransition } from "react";
import { toggleLike } from "./actions";

export function LikeButton({ postId, likes, liked }: {
  postId: string; likes: number; liked: boolean;
}) {
  const [optimistic, addOptimistic] = useOptimistic(
    { likes, liked },
    (state, next: boolean) => ({
      liked: next,
      likes: state.likes + (next ? 1 : -1),
    })
  );
  const [, startTransition] = useTransition();

  return (
    <button
      onClick={() =>
        startTransition(async () => {
          addOptimistic(!optimistic.liked);
          await toggleLike(postId, !optimistic.liked);
        })
      }
    >
      {optimistic.liked ? "❤️" : "🤍"} {optimistic.likes}
    </button>
  );
}

Why You Need a Transition

Optimistic updates must happen inside a transition. Calling addOptimistic outside one throws an error.

  • Use useTransition for button clicks, or
  • Use <form action={...}> — form actions are wrapped in a transition automatically

The transition tells React the update is non-urgent and tied to a pending async action. When the action finishes (and revalidation arrives), the optimistic value is replaced by the real one.

Optimistic Comments with a Form

For adding a comment, the form's action already runs in a transition, so you can call addOptimistic directly inside it.

We append a temporary comment (with a sending flag) so the user sees it immediately, then submit to the server.

"use client";

import { useOptimistic, useRef } from "react";
import { addComment } from "./actions";

type Comment = { id: string; text: string; sending?: boolean };

export function Comments({ postId, comments }: {
  postId: string; comments: Comment[];
}) {
  const formRef = useRef<HTMLFormElement>(null);
  const [optimistic, addOptimistic] = useOptimistic(
    comments,
    (state, text: string) => [
      ...state,
      { id: crypto.randomUUID(), text, sending: true },
    ]
  );

  return (
    <>
      <ul>
        {optimistic.map((c) => (
          <li key={c.id} style={{ opacity: c.sending ? 0.5 : 1 }}>
            {c.text}
          </li>
        ))}
      </ul>
      <form
        ref={formRef}
        action={async (formData) => {
          const text = formData.get("text") as string;
          addOptimistic(text);
          formRef.current?.reset();
          await addComment(postId, text);
        }}
      >
        <input name="text" />
        <button type="submit">Post</button>
      </form>
    </>
  );
}

The Mental Model: Snapshots, Not Mutations

useOptimistic does not change your real state. It layers a temporary view on top.

  • The base state is your prop/state from the server.
  • The optimistic value lives only while a transition is pending.
  • When the transition settles, React throws the optimistic value away and re-renders from the (now updated) base state.

This is why rollback is free: there is nothing to undo, you simply stop showing the optimistic layer.

A Reusable Toggle Reducer

The update function is just a pure reducer. You can model any toggle — likes, bookmarks, follow/unfollow — by switching a boolean and deriving counts.

Because it is pure, you can unit-test it without React. Here is a standalone reducer that mirrors what we pass to useOptimistic.

type ToggleState = { active: boolean; count: number };

function toggleReducer(state: ToggleState, next: boolean): ToggleState {
  return {
    active: next,
    count: state.count + (next ? 1 : -1),
  };
}

const start: ToggleState = { active: false, count: 10 };
const afterLike = toggleReducer(start, true);
const afterUnlike = toggleReducer(afterLike, false);

console.log(afterLike);   // { active: true, count: 11 }
console.log(afterUnlike); // { active: false, count: 10 }

Handling Failures Gracefully

If the Server Action rejects, React reverts the optimistic state, but the user should know why. Wrap the action in try/catch and surface an error (a toast, an inline message).

The optimistic value disappears automatically; your job is only to communicate the failure.

startTransition(async () => {
  addOptimistic(!optimistic.liked);
  try {
    await toggleLike(postId, !optimistic.liked);
  } catch (err) {
    // optimistic value already reverted by React
    toast.error("Could not save your like. Try again.");
  }
});

Keep the Base State in Sync

For automatic reconciliation to work, the base state must come from the server and refresh after the mutation. Two common ways:

  • revalidatePath / revalidateTag inside the Server Action re-fetches the page data.
  • Passing fresh props down to the Client Component (e.g. from a Server Component parent).

If the base state never updates, the optimistic value will vanish and snap back to the old count — a classic flicker bug.

useOptimistic vs useState

You could fake optimism with useState, but you would have to manually save the previous value, write rollback logic, and clear it after the request. That is error-prone.

  • useOptimistic ties the optimistic value to the transition lifecycle — auto-applied while pending, auto-discarded when settled.
  • It re-syncs whenever the base state changes, so server truth always wins.

Reach for useOptimistic whenever an action has a clear, predictable success outcome you can show immediately.

Quick Check

Test your understanding of how useOptimistic behaves.

Recap

You learned how to deliver instant feedback with useOptimistic in Next.js 15:

  • useOptimistic(state, reducer) returns an optimistic view plus an addOptimistic function.
  • Optimistic updates must run inside a transition (via useTransition or a form action).
  • The real mutation lives in a Server Action that calls revalidatePath/revalidateTag so the base state refreshes.
  • On success the optimistic layer is replaced by server truth; on failure React auto-reverts — you just surface an error.
  • Prefer it over manual useState rollback for likes, comments, and toggles.

Questions Fréquemment Posées

La leçon « Retour instantané avec useOptimistic » est-elle gratuite ?

Oui — le texte complet de « Retour instantané avec useOptimistic » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Next.js 15 Fullstack (App Router + Server Actions), passe à CoddyKit PRO. Le cours Next.js 15 Fullstack (App Router + Server Actions) comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Retour instantané avec useOptimistic » ?

Appliquez des mises à jour optimistes de l’interface pour les mentions « J’aime », les commentaires et les boutons à bascule pendant que l’action s’exécute en arrière-plan. Tu pratiques Next.js 15 Fullstack (App Router + Server Actions) avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Next.js 15 Fullstack (App Router + Server Actions) ?

Aucune expérience préalable n'est requise. Next.js 15 Fullstack (App Router + Server Actions) sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Retour instantané avec useOptimistic » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Next.js 15 Fullstack (App Router + Server Actions) ?

Oui. Chaque leçon Next.js 15 Fullstack (App Router + Server Actions) inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Amélioration progressive avec la propriété Action des formulaires
  2. États d’attente et de chargement avec useFormStatus
  3. Erreurs de validation au niveau des champs avec useActionState
  4. Retour instantané avec useOptimistic
← Retour à Next.js 15 Fullstack (App Router + Server Actions)