typescript 118 lines · 3 tabs

Optimistic Todo Toggling in React with Rollback via useReducer

Shared by codesnips Jul 2026
3 tabs
export interface Todo {
  id: string;
  title: string;
  done: boolean;
}

export type Action =
  | { type: "set"; todos: Todo[] }
  | { type: "toggle"; id: string }
  | { type: "delete"; id: string }
  | { type: "replace"; todo: Todo }
  | { type: "revert"; todos: Todo[] };

export function todoReducer(state: Todo[], action: Action): Todo[] {
  switch (action.type) {
    case "set":
    case "revert":
      return action.todos;
    case "toggle":
      return state.map((t) =>
        t.id === action.id ? { ...t, done: !t.done } : t
      );
    case "delete":
      return state.filter((t) => t.id !== action.id);
    case "replace":
      return state.map((t) => (t.id === action.todo.id ? action.todo : t));
    default:
      return state;
  }
}
3 files · typescript Explain with highlit

Optimistic UI is a pattern where the interface updates immediately in response to a user action, before the server has confirmed the change. This makes an app feel instant even over a slow network, because the perceived latency drops to zero. The catch is that the network request can still fail, so the UI must be able to roll back to the state it had before the optimistic edit. This snippet shows a small todo list that toggles and deletes items optimistically, then reverts precisely when a request fails.

The todoReducer tab centralizes all state transitions in a useReducer. Rather than sprinkling setState calls around, every mutation is an explicit action: toggle flips done locally, delete removes the row, and replace swaps in an authoritative version from the server. The important detail is revert, which restores a caller-provided snapshot. Because the reducer is a pure function, computing the next state and capturing the previous one is trivial, and there is a single place where the shape of a Todo is understood. Keeping the reducer pure also makes rollback deterministic: given the same snapshot the same state is restored every time.

The useTodos hook tab wires the reducer to the API and owns the optimistic protocol. Each mutating handler follows the same three steps. First it captures a snapshot of the current list into a local variable before dispatching, which is why todosRef mirrors the latest state — reading state directly inside an async callback would risk a stale closure. Second it dispatches the optimistic action so the UI updates synchronously. Third it awaits the network call inside a try/catch; on failure it dispatches revert with the captured snapshot and surfaces an error. The toggle handler shows an extra refinement: on success it dispatches replace with the server's canonical row, so any server-side derived fields stay consistent instead of trusting the local guess.

A subtle correctness point handled here is concurrency. Two rapid toggles could each capture a snapshot and, if the first fails after the second succeeds, revert too far. The hook guards against the worst of this by keying in-flight operations in pending and only reverting when an item is still meaningfully outstanding, and by having the failing operation restore just its own item rather than the whole list where possible.

The TodoList component tab is deliberately thin: it renders from todos, disables a row while it is pending, and calls the hook's handlers. Because the optimistic logic lives in the hook, the component never needs to know about snapshots or rollbacks — it simply reflects state. This separation is the main trade-off of the pattern: extra bookkeeping (snapshots, pending flags, error surfacing) in exchange for a UI that responds instantly and still stays truthful when the backend disagrees. It is the right approach for high-frequency, low-stakes edits like toggles and reorders, and less appropriate for actions where showing an unconfirmed result would be misleading, such as payments.


Related snips

typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
javascript
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
  const success = true;

  setTimeout(() => {
    if (success) {

Promises and async/await patterns for asynchronous JavaScript

javascript promises async-await
by Alex Chang 1 tab
javascript
import { Controller } from "@hotwired/stimulus"
import Mousetrap from "mousetrap"

export default class extends Controller {
  connect() {
    // Global shortcuts

Keyboard shortcuts with Stimulus and Mousetrap

stimulus javascript ux
by Jordan Lee 2 tabs
typescript
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
  timeout: 15000,

Axios API client with interceptors

react axios api
by Maya Patel 1 tab
typescript
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface FilterState {
  search: string
  category: string | null

Zustand for lightweight state management

react zustand state-management
by Maya Patel 2 tabs
swift
import SwiftUI

struct ContentView: View {
    @State private var username = ""
    @State private var isLoggedIn = false
    @StateObject private var viewModel = LoginViewModel()

SwiftUI declarative UI with state management

swift swiftui ios
by Sofia Martinez 2 tabs

Share this code

Here's the card — post it anywhere.

Optimistic Todo Toggling in React with Rollback via useReducer — share card
Link copied