javascript
44 lines · 7 steps
Optimistic like buttons in Next.js
A client component that flips the UI instantly with useOptimistic, then reconciles with a server action.
Explained by
highlit
1'use client';
2
3import { useOptimistic, useTransition } from 'react';
4import { toggleLike } from '@/app/actions/likes';
5
6export function LikeButton({ postId, initialLiked, initialCount }) {
7 const [isPending, startTransition] = useTransition();
8 const [optimistic, setOptimistic] = useOptimistic(
9 { liked: initialLiked, count: initialCount },
10 (state, liked) => ({
11 liked,
12 count: state.count + (liked ? 1 : -1),
13 })
14 );
15
16 function handleClick() {
17 const nextLiked = !optimistic.liked;
18 startTransition(async () => {
19 setOptimistic(nextLiked);
20 await toggleLike(postId, nextLiked);
21 });
22 }
23
24 return (
25 <button
26 type="button"
27 onClick={handleClick}
28 disabled={isPending}
29 aria-pressed={optimistic.liked}
30 className="flex items-center gap-1.5 text-sm font-medium disabled:opacity-60"
31 >
32 <svg
33 viewBox="0 0 24 24"
34 className={`h-5 w-5 transition-colors ${
35 optimistic.liked ? 'fill-red-500 stroke-red-500' : 'fill-none stroke-current'
36 }`}
37 strokeWidth={2}
38 >
39 <path d="M12 21s-7.5-4.6-10-9.3C.7 8.9 2.2 5.5 5.5 5.5c2 0 3.4 1.2 4.5 2.8 1.1-1.6 2.5-2.8 4.5-2.8 3.3 0 4.8 3.4 3.5 6.2C19.5 16.4 12 21 12 21z" />
40 </svg>
41 <span>{optimistic.count}</span>
42 </button>
43 );
44}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1useOptimistic lets you render a provisional state that automatically reverts if the underlying data doesn't confirm it.
- 2Wrapping a server action in startTransition keeps the UI responsive and exposes a pending flag for disabling controls.
- 3Deriving both the icon fill and count from one optimistic value keeps the whole button visually consistent during the round trip.
Related explainers
javascript
const express = require('express'); const router = express.Router(); const { pool } = require('../db'); const redis = require('../redis');
Building a health check endpoint in Express
health-check
timeouts
promise-race
Intermediate
9 steps
javascript
'use client'; import { useEffect } from 'react'; import * as Sentry from '@sentry/nextjs';
How a Next.js error boundary recovers
error-boundary
error-handling
observability
Intermediate
8 steps
javascript
const MAX_BATCH_SIZE = 20; const FLUSH_INTERVAL = 5000; const ENDPOINT = '/api/analytics';
Batching analytics events in the browser
batching
buffering
sendbeacon
Intermediate
10 steps
javascript
export function diffIds(current, desired) { const currentSet = new Set(current); const desiredSet = new Set(desired);
Diffing sets to sync group membership
set-operations
diffing
transactions
Intermediate
8 steps
javascript
class HistoryStack { constructor(initial = '', limit = 100) { this.past = []; this.present = initial;
Building an undo/redo history stack
undo-redo
state-management
debouncing
Intermediate
7 steps
javascript
import { useState, useCallback, useRef } from 'react'; function LikeButton({ postId, initialLiked, initialCount }) { const [liked, setLiked] = useState(initialLiked);
Optimistic like button with React
optimistic-ui
abortcontroller
error-handling
Advanced
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/optimistic-like-buttons-in-next-js-explained-javascript-e9c2/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.