Skip to content

Remix Optimistic UI — Instant Mutation Feedback

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Remix Optimistic UI. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Remix optimistic UI: update the UI immediately during mutations, show pending state, and handle rollbacks on server errors for responsive interfaces.

In this lesson, you'll implement optimistic UI that updates instantly before the server confirms, with automatic rollback if the mutation fails.

What You'll Learn

How to use useFetcher for optimistic updates, show pending UI state, handle rollbacks on failure, and combine with server validation.

Why It Matters

Optimistic UI makes apps feel instant by showing the expected result immediately, then correcting if the server rejects the change. This dramatically improves perceived performance.

Real-World Use

DodaZIP uses optimistic UI for toggling feature flags and marking notifications as read—actions that usually succeed but need fallback.

flowchart LR
    A[User Action] --> B[Update UI Immediately]
    B --> C[Send Mutation]
    C -->|Success| D[Confirm UI]
    C -->|Failure| E[Rollback UI]
    E --> F[Show Error]
    style B fill:#121212,color:#fff

Basic Optimistic Form

import { Form, useNavigation } from "@remix-run/react";

export default function TodoItem({ todo }) {
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";

  return (
    <div>
      <span style={{ opacity: isSubmitting ? 0.5 : 1 }}>
        {todo.title}
      </span>
      <Form method="post" style={{ display: "inline" }}>
        <input type="hidden" name="id" value={todo.id} />
        <button type="submit" name="intent" value="toggle">
          {isSubmitting ? "..." : todo.completed ? "Undo" : "Done"}
        </button>
      </Form>
    </div>
  );
}

Optimistic UI with useFetcher

import { useFetcher } from "@remix-run/react";

export default function LikeButton({ post }) {
  const fetcher = useFetcher();
  const optimisticLikes = fetcher.formData
    ? parseInt(fetcher.formData.get("likes")) 
    : post.likes;
  const isLiked = fetcher.formData
    ? fetcher.formData.get("liked") === "true"
    : post.isLiked;

  return (
    <fetcher.Form method="post" action={`/posts/${post.id}/like`}>
      <input type="hidden" name="likes" value={post.likes + 1} />
      <input type="hidden" name="liked" value={!post.isLiked} />
      <button type="submit">
        {optimisticLikes} {isLiked ? "Liked" : "Like"}
      </button>
    </fetcher.Form>
  );
}

The UI updates immediately with the optimistic value. If the server rejects it, the loader re-runs and corrects the display.

Pending UI with Navigation

import { useNavigation } from "@remix-run/react";

export default function SubmitButton() {
  const navigation = useNavigation();
  const isPending = navigation.state === "submitting"
    || navigation.state === "loading";

  return (
    <button type="submit" disabled={isPending}>
      {isPending ? "Saving..." : "Save"}
    </button>
  );
}

Optimistic List Operations

export default function TodoList({ todos }) {
  const fetcher = useFetcher();

  const optimisticTodos = fetcher.formData
    ? todos.filter(t => t.id !== fetcher.formData.get("deleteId"))
    : todos;

  return (
    <ul>
      {optimisticTodos.map(todo => (
        <li key={todo.id}>
          {todo.title}
          <fetcher.Form method="post" style={{ display: "inline" }}>
            <input type="hidden" name="deleteId" value={todo.id} />
            <button type="submit" name="intent" value="delete">Delete</button>
          </fetcher.Form>
        </li>
      ))}
    </ul>
  );
}

Handling Rollbacks

The loader automatically provides correct data after mutation, so rollback is automatic. On failure, the action returns an error, and the loader data refreshes with the original values.

Common Mistakes

  1. Not using fetcher.formData for optimistic values: fetcher.formData contains the submitted data. Use it to compute the optimistic state.
  2. Optimistic updates on critical mutations: Don't use optimism for irreversible actions (payment, account deletion). Wait for server confirmation.
  3. Forgetting to disable buttons during submission: Without disabling, users can submit the same form multiple times.
  4. Not handling optimistic errors gracefully: Show error messages and revert UI on server failure using action return data.
  5. Over-engineering optimistic state: Start simple with the pending state pattern. Add optimistic fetcher.formData only for high-frequency interactions.

Practice Questions

  1. What hook provides access to the form data being submitted? Answer: useFetcher() returns formData property with the data being submitted to the action.

  2. How does Remix revert optimistic UI on error? Answer: The loader re-runs after the action returns. The new loader data overwrites the optimistic state with the correct server state.

  3. What is the difference between useNavigation and useFetcher? Answer: useNavigation works with <Form> and page transitions. useFetcher works with <fetcher.Form> for non-navigation mutations.

  4. When should you NOT use optimistic UI? Answer: For irreversible operations like payments, account deletion, or any mutation with high failure probability.

Challenge

Build a task list with optimistic toggles: clicking "Complete" immediately shows it as completed, changing the UI instantly. If the server fails, revert and show an error message.

Mini Project

Create a social news feed with optimistic upvotes and comments. Upvotes update instantly, new comments appear immediately, and errors trigger a rollback with notification.

FAQ

Does optimistic UI work without JavaScript?

: No. Optimistic UI requires JavaScript. The server-rendered page shows the actual state for non-JS users.

Can I use optimistic UI with file uploads?

: Yes. Show the file thumbnail immediately while the upload processes in the background.

How do I handle multiple optimistic mutations?

: Each mutation uses its own fetcher instance. Track optimistic state per mutation independently.

What happens if the server is slow?

: The optimistic state persists until the server responds. Consider adding a timeout-based fallback.

What's Next

Learn about Remix Pending UI for showing loading states during navigation and data loading.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro