PatternAPIUXState

Optimistic Updates Pattern

A pattern for implementing optimistic UI updates that instantly reflect changes while syncing with the server in the background.

Preview

Code

import { useState, useCallback } from 'react'

interface OptimisticState<T> {
  data: T
  pending: boolean
  error: Error | null
}

export function useOptimistic<T>(
  initialData: T,
  updateFn: (data: T) => Promise<T>
): {
  state: OptimisticState<T>
  update: (optimisticData: T) => Promise<void>
  reset: () => void
} {
  const [state, setState] = useState<OptimisticState<T>>({
    data: initialData,
    pending: false,
    error: null,
  })

  const [previousData, setPreviousData] = useState<T>(initialData)

  const update = useCallback(
    async (optimisticData: T) => {
      // Store previous data for rollback
      setPreviousData(state.data)

      // Optimistically update UI
      setState({
        data: optimisticData,
        pending: true,
        error: null,
      })

      try {
        // Sync with server
        const serverData = await updateFn(optimisticData)
        setState({
          data: serverData,
          pending: false,
          error: null,
        })
      } catch (error) {
        // Rollback on error
        setState({
          data: previousData,
          pending: false,
          error: error instanceof Error ? error : new Error('Update failed'),
        })
      }
    },
    [state.data, previousData, updateFn]
  )

  const reset = useCallback(() => {
    setState({
      data: initialData,
      pending: false,
      error: null,
    })
  }, [initialData])

  return { state, update, reset }
}

// Usage example:
function LikeButton({ postId, initialLikes }: { postId: string; initialLikes: number }) {
  const { state, update } = useOptimistic(
    { likes: initialLikes, liked: false },
    async (data) => {
      const res = await fetch(`/api/posts/${postId}/like`, {
        method: data.liked ? 'POST' : 'DELETE',
      })
      return res.json()
    }
  )

  const handleClick = () => {
    update({
      likes: state.data.liked ? state.data.likes - 1 : state.data.likes + 1,
      liked: !state.data.liked,
    })
  }

  return (
    <button onClick={handleClick} disabled={state.pending}>
      {state.data.liked ? '❤️' : '🤍'} {state.data.likes}
      {state.error && <span className="text-red-500">Failed to update</span>}
    </button>
  )
}

Looking for more?

Browse the full collection of patterns or check out other exploration topics.