javascript
63 lines · 8 steps
Preventing re-renders in a React todo list
A memoized row plus stable useCallback handlers keep unchanged todo items from re-rendering on every keystroke.
Explained by
highlit
1import { useState, useCallback, memo } from 'react';
2
3const TodoItem = memo(function TodoItem({ todo, onToggle, onRemove }) {
4 return (
5 <li className={todo.done ? 'done' : ''}>
6 <label>
7 <input
8 type="checkbox"
9 checked={todo.done}
10 onChange={() => onToggle(todo.id)}
11 />
12 {todo.text}
13 </label>
14 <button onClick={() => onRemove(todo.id)}>×</button>
15 </li>
16 );
17});
18
19export default function TodoList() {
20 const [todos, setTodos] = useState([]);
21 const [draft, setDraft] = useState('');
22
23 const addTodo = useCallback(() => {
24 const text = draft.trim();
25 if (!text) return;
26 setTodos((prev) => [...prev, { id: crypto.randomUUID(), text, done: false }]);
27 setDraft('');
28 }, [draft]);
29
30 const toggleTodo = useCallback((id) => {
31 setTodos((prev) =>
32 prev.map((t) => (t.id === id ? { ...t, done: !t.done } : t))
33 );
34 }, []);
35
36 const removeTodo = useCallback((id) => {
37 setTodos((prev) => prev.filter((t) => t.id !== id));
38 }, []);
39
40 return (
41 <section>
42 <div className="composer">
43 <input
44 value={draft}
45 onChange={(e) => setDraft(e.target.value)}
46 onKeyDown={(e) => e.key === 'Enter' && addTodo()}
47 placeholder="What needs doing?"
48 />
49 <button onClick={addTodo}>Add</button>
50 </div>
51 <ul>
52 {todos.map((todo) => (
53 <TodoItem
54 key={todo.id}
55 todo={todo}
56 onToggle={toggleTodo}
57 onRemove={removeTodo}
58 />
59 ))}
60 </ul>
61 </section>
62 );
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1memo only helps when the props you pass are referentially stable across renders.
- 2Wrapping handlers in useCallback with the right dependencies gives child components props that don't change every render.
- 3Updating state immutably with map and filter lets React detect exactly which items changed.
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/preventing-re-renders-in-a-react-todo-list-explained-javascript-8592/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.