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
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
javascript
import { useEffect, useRef, useState } from 'react'; export function useDelayedFlag(active, delay = 300) { const [visible, setVisible] = useState(false);
Delaying a loading spinner with a React hook
custom-hooks
debouncing
cleanup
Intermediate
8 steps
javascript
const SWIPE_THRESHOLD = 80; const MAX_TRANSLATE = 120; export function attachSwipeToDismiss(element, onDismiss) {
Building a swipe-to-dismiss gesture in JS
touch-events
gesture-detection
dom-manipulation
Intermediate
10 steps
javascript
const { pool } = require('./db'); function withTransaction() { return async (req, res, next) => {
A per-request transaction middleware in Express
middleware
database-transactions
connection-pooling
Advanced
7 steps
javascript
function collapseConsecutiveLogs(lines, { keyFn = (l) => l.message } = {}) { const groups = []; for (const line of lines) {
Collapsing consecutive log lines in JavaScript
grouping
run-length-encoding
data-transformation
Intermediate
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.