|
| 1 | ++++ |
| 2 | +title = "Taming React Re-renders: A Guide to Optimizing Performance" |
| 3 | +slug = "taming-react-re-renders" |
| 4 | +date = 2025-10-05T20:48:34+05:30 |
| 5 | +image = "/images/2025/taming-react-re-renders/header.jpg" |
| 6 | +draft = false |
| 7 | +authors = ["Ajith Kumar"] |
| 8 | +description = "A comprehensive guide to understanding and optimizing React re-renders for better application performance" |
| 9 | +tags = ["React", "Performance", "Software Craftsmanship"] |
| 10 | +categories = ["React", "Performance", "Software Craftsmanship"] |
| 11 | +type = "" |
| 12 | ++++ |
| 13 | + |
| 14 | +Have you ever noticed your React app feeling sluggish? or wondered why your components keep re-rendering when they shouldn't? You're not alone! |
| 15 | + |
| 16 | +## The Problem: Unnecessary Re-renders |
| 17 | + |
| 18 | +Let's start with a simple example. Imagine you're building a todo list app: |
| 19 | + |
| 20 | +```jsx |
| 21 | +function TodoApp() { |
| 22 | + const [todos, setTodos] = useState([]); |
| 23 | + const [text, setText] = useState(''); |
| 24 | + |
| 25 | + const addTodo = () => { |
| 26 | + const newTodo = { |
| 27 | + id: Date.now(), |
| 28 | + text: text |
| 29 | + }; |
| 30 | + setTodos([...todos, newTodo]); |
| 31 | + setText(''); |
| 32 | + }; |
| 33 | + |
| 34 | + return ( |
| 35 | + <div> |
| 36 | + <input |
| 37 | + value={text} |
| 38 | + onChange={(e) => setText(e.target.value)} |
| 39 | + placeholder='Add todoβ¦' |
| 40 | + /> |
| 41 | + <button onClick={addTodo}>Add</button> |
| 42 | + <TodoList items={todos} /> |
| 43 | + </div> |
| 44 | + ); |
| 45 | +} |
| 46 | + |
| 47 | +function TodoList({items}) { |
| 48 | + return ( |
| 49 | + <ul> |
| 50 | + {items.map((item) => ( |
| 51 | + <li key={item.id}>{item.text}</li> |
| 52 | + ))} |
| 53 | + </ul> |
| 54 | + ); |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +Notice something interesting? Every time you type in the input field, the entire `TodoList` re-renders, even though you haven't added any new todos! This happens because React re-renders the parent component (`TodoApp`) when its state changes, which then re-renders all its children. |
| 59 | + |
| 60 | +## Understanding React's Lifecycle |
| 61 | + |
| 62 | +To fix unnecessary re-renders, we need to understand how React works. Think of React components like a tree: |
| 63 | + |
| 64 | +- When a component first appears on screen, it **mounts** |
| 65 | +- When its data (state or props) changes, it **updates** |
| 66 | +- When it's removed from the screen, it **unmounts** |
| 67 | + |
| 68 | +Here's what you need to know: |
| 69 | + |
| 70 | +- **State changes** trigger re-renders |
| 71 | + |
| 72 | +{{< figure src="/images/2025/taming-react-re-renders/state-changes-example.jpg" caption="" >}} |
| 73 | + |
| 74 | +When a component's state changes, React automatically re-renders that component. This is React's way of keeping the UI in sync with your data. For example, when you type in an input field, the component holding that input's state will re-render to reflect the new value. |
| 75 | + |
| 76 | +- **Parent updates** cause child re-renders |
| 77 | + |
| 78 | +{{< figure src="/images/2025/taming-react-re-renders/parent-example.jpg" caption="" >}} |
| 79 | + |
| 80 | +React follows a top-down rendering pattern. When a parent component re-renders, all of its children re-render too (unless they're memoized). This is why moving state down the component tree can be so effective - it limits the scope of re-renders to only the components that actually need to update. |
| 81 | + |
| 82 | +- React is smart! It batches multiple state updates in event handlers into a single re-render |
| 83 | + |
| 84 | +## Watch Out for Custom Hooks! |
| 85 | + |
| 86 | +Custom hooks are great for reusing logic, but they can cause performance issues behind your back. Here's an example: |
| 87 | + |
| 88 | +```jsx |
| 89 | +function useWindowWidth() { |
| 90 | + const [width, setWidth] = useState(window.innerWidth); |
| 91 | + useEffect(() => { |
| 92 | + const onResize = () => setWidth(window.innerWidth); |
| 93 | + window.addEventListener('resize', onResize); |
| 94 | + return () => window.removeEventListener('resize', onResize); |
| 95 | + }, []); |
| 96 | + return width; |
| 97 | +} |
| 98 | + |
| 99 | +function Header() { |
| 100 | + const width = useWindowWidth(); |
| 101 | + console.log('Header render'); |
| 102 | + return <h1>Window: {width}px</h1>; |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +Every time you resize your browser window, even by just 1 pixel, the `Header` component re-renders! If you use this hook in multiple components, you could end up with a lot of unnecessary re-renders. |
| 107 | + |
| 108 | +To avoid this: |
| 109 | + |
| 110 | +- Use throttling or debouncing for frequent updates |
| 111 | +- Only use hooks in components that really need them |
| 112 | + |
| 113 | +Here's how to implement throttling to prevent excessive re-renders: |
| 114 | + |
| 115 | +```jsx |
| 116 | +import {useState, useEffect, useRef, useCallback} from 'react'; |
| 117 | +import {throttle} from 'lodash'; |
| 118 | + |
| 119 | +function useThrottledWindowWidth(delay = 100) { |
| 120 | + // 1) Initialize state safely (SSR-friendly) |
| 121 | + const [width, setWidth] = useState(() => |
| 122 | + typeof window !== 'undefined' ? window.innerWidth : 0 |
| 123 | + ); |
| 124 | + |
| 125 | + // 2) Create a stable, memoized throttled handler |
| 126 | + const throttled = useRef( |
| 127 | + throttle(() => { |
| 128 | + setWidth(window.innerWidth); |
| 129 | + }, delay) |
| 130 | + ); |
| 131 | + |
| 132 | + // 3) Whenever `delay` changes, re-create the throttle function |
| 133 | + useEffect(() => { |
| 134 | + throttled.current = throttle(() => setWidth(window.innerWidth), delay); |
| 135 | + return () => throttled.current.cancel(); |
| 136 | + }, [delay]); |
| 137 | + |
| 138 | + // 4) Wire up the resize listener once |
| 139 | + useEffect(() => { |
| 140 | + if (typeof window === 'undefined') return; |
| 141 | + |
| 142 | + const handler = () => throttled.current(); |
| 143 | + window.addEventListener('resize', handler); |
| 144 | + return () => { |
| 145 | + window.removeEventListener('resize', handler); |
| 146 | + throttled.current.cancel(); |
| 147 | + }; |
| 148 | + }, []); |
| 149 | + |
| 150 | + return width; |
| 151 | +} |
| 152 | + |
| 153 | +// Usage remains the same: |
| 154 | +function Header() { |
| 155 | + const width = useThrottledWindowWidth(100); |
| 156 | + console.log('Header render'); |
| 157 | + return <h1>Window: {width}px</h1>; |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +## The "Big Re-renders" Myth |
| 162 | + |
| 163 | +Many developers worry about "big" components causing performance issues. But here's the truth: React's diffing algorithm is very efficient! Even lists with dozens of items render quickly. |
| 164 | + |
| 165 | +{{< figure src="/images/2025/taming-react-re-renders/props-myth.jpg" caption="" >}} |
| 166 | + |
| 167 | +This image indicates that when you pass an object as a prop using an inline object literal (e.g., `<Child value={{value}} />`), a new object is created on every render, even if its values haven't changed. React uses shallow comparison to check for prop changes. Since the object reference is different on each render, React thinks the prop has changed and re-renders the child component. |
| 168 | + |
| 169 | +Instead of worrying about component size, focus on: |
| 170 | + |
| 171 | +- Optimizing parent components |
| 172 | +- Stabilizing prop references |
| 173 | +- Using memoization when it makes sense |
| 174 | + |
| 175 | +Here's how to use memoization: |
| 176 | + |
| 177 | +```jsx |
| 178 | +const MemoizedList = React.memo(function ({items}) { |
| 179 | + console.log('List render'); |
| 180 | + return null; |
| 181 | +}); |
| 182 | + |
| 183 | +// In parent: |
| 184 | +const stableItems = useMemo(() => items, [items]); |
| 185 | +<MemoizedList items={stableItems} />; |
| 186 | +``` |
| 187 | + |
| 188 | +## The solution: Moving State Down |
| 189 | + |
| 190 | +One of the best ways to prevent unnecessary re-renders is to move state as close as possible to where it's used. Here's an example: |
| 191 | + |
| 192 | +```jsx |
| 193 | +// Before: parent holds all item-states |
| 194 | +function Parent({initialItems}) { |
| 195 | + const [items, setItems] = useState(initialItems); |
| 196 | + return items.map((item, i) => ( |
| 197 | + <Item |
| 198 | + key={i} |
| 199 | + item={item} |
| 200 | + onUpdate={(newItem) => { |
| 201 | + const copy = [...items]; |
| 202 | + copy[i] = newItem; |
| 203 | + setItems(copy); |
| 204 | + }} |
| 205 | + /> |
| 206 | + )); |
| 207 | +} |
| 208 | + |
| 209 | +// After: each Item manages its own state |
| 210 | +function Parent({initialItems}) { |
| 211 | + return initialItems.map((item, i) => <Item key={i} initial={item} />); |
| 212 | +} |
| 213 | + |
| 214 | +function Item({initial}) { |
| 215 | + const [item, setItem] = useState(initial); |
| 216 | + return ( |
| 217 | + <div> |
| 218 | + <input |
| 219 | + value={item.text} |
| 220 | + onChange={(e) => setItem({...item, text: e.target.value})} |
| 221 | + /> |
| 222 | + </div> |
| 223 | + ); |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +## Parting thoughts: |
| 228 | + |
| 229 | +By moving state down to individual `Item` components, typing in one input only re-renders that specific item, not the entire list! |
| 230 | + |
| 231 | +**Remember**: Optimizing React performance is more about understanding when and why components re-render than about complex optimizations. Start by profiling your app with [React DevTools](https://react.dev/learn/react-developer-tools), identify the bottlenecks, and apply these strategies where they make the most sense. |
| 232 | + |
| 233 | +For even better debugging, check out [why-did-you-render](https://github.com/welldone-software/why-did-you-render) - a fantastic tool that logs when and why your components re-render, making it much easier to spot unnecessary re-renders in development. |
| 234 | + |
| 235 | +Happy coding! π |
0 commit comments