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
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
typescript
import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'chunk',
A memoized chunk pipe in Angular
memoization
weakmap
pure-pipes
Intermediate
7 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
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.