Skip to content

Optimistic Updates

An optimistic update changes the UI before the server responds. If the server succeeds, nothing changes — the UI was already right. If the server fails, you roll back. This makes interactions feel instant instead of sluggish.

// src/api/postsApi.ts (add to existing file)
export interface UpvoteResponse {
upvotes: number
}
export async function upvotePost(id: number): Promise<UpvoteResponse> {
const res = await apiClient.post<UpvoteResponse>(`/posts/${id}/upvote`)
return res.data
}

The server returns the new vote count — you’ll use this to reconcile state.

Add upvote functionality directly to the post list. Each post needs its own vote state:

// src/components/PostFeed.tsx — updated
import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import { getPosts, upvotePost, Post } from '../api/postsApi'
export default function PostFeed() {
const [posts, setPosts] = useState<Post[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const { isAuthenticated } = useAuth()
useEffect(() => {
getPosts()
.then(setPosts)
.catch(() => setError('Failed to load posts.'))
.finally(() => setLoading(false))
}, [])
async function handleUpvote(postId: number) {
// Optimistic update — increment immediately
setPosts(prev =>
prev.map(p => p.id === postId ? { ...p, upvotes: p.upvotes + 1 } : p)
)
try {
const { upvotes } = await upvotePost(postId)
// Reconcile with server value
setPosts(prev =>
prev.map(p => p.id === postId ? { ...p, upvotes } : p)
)
} catch {
// Rollback
setPosts(prev =>
prev.map(p => p.id === postId ? { ...p, upvotes: p.upvotes - 1 } : p)
)
}
}
if (loading) return <p>Loading...</p>
if (error) return <p>{error}</p>
return (
<ul>
{posts.map(post => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>
<strong>{post.title}</strong>
</Link>
<span> by {post.username}</span>
{isAuthenticated && (
<button onClick={() => handleUpvote(post.id)}>
{post.upvotes}
</button>
)}
{!isAuthenticated && <span> · {post.upvotes} votes</span>}
<span> · {post.commentCount} comments</span>
</li>
))}
</ul>
)
}
1. Update state immediately (optimistic)
2. Send the request
3a. Success → reconcile with server value
3b. Failure → rollback

Step 3a is important even on success: the server might have a different vote count than you assumed (e.g., a race condition where another user voted at the same moment). Using the server’s returned count keeps your UI accurate.

setPosts(prev =>
prev.map(p => p.id === postId ? { ...p, upvotes: p.upvotes + 1 } : p)
)

This pattern never mutates p directly — it creates a new object with { ...p, upvotes: ... }. React detects the change and re-renders. Mutating the object directly (p.upvotes++) would not trigger a re-render.

Use them for fast, reversible actions where the server is very likely to succeed:

  • Upvotes, likes, reactions
  • Bookmark / follow toggles
  • Simple preference changes

Avoid them for actions where failure is common or costly:

  • Creating new data (a failed post create needs its own error UI)
  • Deleting (wait for the server before removing from the list)
  • Anything involving payment or sensitive data

Open src/components/PostFeed.tsx in your Bulletin project.

  1. Add upvotePost to src/api/postsApi.ts.
  2. Update PostFeed.tsx to include handleUpvote with the three-step optimistic pattern.
  3. Add the upvote button — show it only when isAuthenticated, show a plain count otherwise.
  4. Test it: click the upvote button on a post and confirm the count updates immediately. Open your SQLite database or call GET /posts/:id in Thunder Client and confirm the count also updated on the server.
  5. To test rollback: temporarily change upvotePost to throw an error and confirm the count reverts after clicking.
  • Optimistic updates change state before the server responds — rollback on failure.
  • Always reconcile with the server’s returned value, even on success.
  • Use { ...p, field: newValue } to update a single item in an array immutably.
  • Best suited for fast, reversible, low-risk actions like upvotes.