Tools: Referential Equality & Memoization: Why `{} !== {}` Breaks React Performance

Tools: Referential Equality & Memoization: Why `{} !== {}` Breaks React Performance

Source: Dev.to

The Golden Rule ## Part 1: Referential Equality Explained ## Primitives vs Objects ## What About Same Reference? ## Part 2: How This Affects React ## Example: React.memo Doesn't Work ## The Same Problem with Arrays ## And Functions Too! ## Part 3: Memoization to the Rescue ## 1. useMemo for Objects and Arrays ## With Dependencies ## 2. useCallback for Functions ## With Dependencies ## 3. React.memo for Components ## Part 4: When to Use Memoization ## Use Memoization When: ## Don't Use Memoization When: ## Part 5: Common Memoization Pitfalls ## Pitfall 1: Missing Dependencies ## Pitfall 2: Memoizing Without Memoized Child ## Pitfall 3: Creating Objects Inside JSX ## Pitfall 4: Memoizing Everything (Over-Optimization) ## Part 6: Memoization Patterns in React ## Pattern 1: Memoizing Context Values ## Pattern 2: Memoizing Expensive Filters ## Pattern 3: Memoizing Event Handlers with Parameters ## Pattern 4: Using Refs for Stable References ## Part 7: Debugging Referential Equality ## Tool 1: Object.is() (React's Comparison) ## Tool 2: React DevTools Profiler ## Tool 3: useWhyDidYouUpdate Hook (Custom) ## Quick Reference Cheat Sheet ## Key Takeaways ## Interview Tip You've probably seen code like this in React: And wondered why ChildComponent re-renders on every parent render, even when wrapped in React.memo. Or you've used useEffect and seen it run repeatedly: The culprit? Referential equality — JavaScript's way of comparing objects and arrays by reference, not by value. Understanding this is critical for writing performant React applications. In JavaScript, objects and arrays are compared by reference (memory address), not by value. Two objects with identical contents are NOT equal if they're different instances. This means {} !== {} and [] !== []. React uses referential equality to determine when to re-render components, so creating new objects/arrays on every render can cause unnecessary re-renders. In simpler terms: JavaScript doesn't care if two objects look the same — it only cares if they're the SAME object in memory. Let's understand why this matters and how memoization solves it. Primitives (numbers, strings, booleans, null, undefined) are compared by value: Objects (including arrays, functions) are compared by reference: Why? obj1 and obj2 are stored at different memory addresses, even though they have identical contents. Key Point: When you assign an object to another variable, you're copying the reference, not the object itself. (See the "Pass by Value" article for more on this!) React uses referential equality to determine if props have changed: Result: Every time you click the button, MemoizedChild re-renders, even though React.memo is supposed to prevent that. Same issue: items is a new array on every render, so MemoizedList always re-renders. Same issue: handleClick is a new function on every render. Memoization is the technique of caching values/computations so they're only recalculated when dependencies change. React provides three hooks for memoization: useCallback is shorthand for memoizing functions: Equivalent useMemo version: But useCallback is cleaner for functions. React.memo is a Higher-Order Component that memoizes the entire component: Important: React.memo uses shallow comparison: Don't memoize everything! Only optimize when you have a performance problem. Problem: handleClick closes over the initial userId and never updates. Problem: Child re-renders on every Parent render anyway (not wrapped in React.memo). Problem: useMemo has overhead. For simple computations, it's slower than just recalculating. Sometimes you need a value that doesn't trigger re-renders: Use case: When you need a stable reference but don't want to trigger re-renders when it changes. React uses Object.is() for comparison (similar to === but handles NaN correctly): Use the React DevTools Profiler to see why components re-render: Objects and arrays are compared by reference, not by value: {} !== {} Creating objects/arrays/functions on every render causes unnecessary re-renders in memoized components useMemo stabilizes object/array references across renders useCallback stabilizes function references (shorthand for useMemo with functions) React.memo prevents component re-renders when props don't change (by reference) Always include dependencies in useMemo and useCallback Don't memoize primitives — they're already compared by value Memoize context values to prevent unnecessary consumer re-renders Don't over-optimize — memoization has overhead, only use it when needed When asked about referential equality and memoization: Now go forth and never wonder why your memoized component keeps re-rendering again!h Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK: <ChildComponent config={{ theme: 'dark' }} /> Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: <ChildComponent config={{ theme: 'dark' }} /> CODE_BLOCK: <ChildComponent config={{ theme: 'dark' }} /> COMMAND_BLOCK: useEffect(() => { fetchData(); }, [config]); // 'config' is recreated every render! Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: useEffect(() => { fetchData(); }, [config]); // 'config' is recreated every render! COMMAND_BLOCK: useEffect(() => { fetchData(); }, [config]); // 'config' is recreated every render! CODE_BLOCK: const a = 5; const b = 5; console.log(a === b); // true const str1 = 'hello'; const str2 = 'hello'; console.log(str1 === str2); // true Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const a = 5; const b = 5; console.log(a === b); // true const str1 = 'hello'; const str2 = 'hello'; console.log(str1 === str2); // true CODE_BLOCK: const a = 5; const b = 5; console.log(a === b); // true const str1 = 'hello'; const str2 = 'hello'; console.log(str1 === str2); // true CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = { name: 'Alice' }; console.log(obj1 === obj2); // false (different references!) const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; console.log(arr1 === arr2); // false Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = { name: 'Alice' }; console.log(obj1 === obj2); // false (different references!) const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; console.log(arr1 === arr2); // false CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = { name: 'Alice' }; console.log(obj1 === obj2); // false (different references!) const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; console.log(arr1 === arr2); // false CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = obj1; // Same reference console.log(obj1 === obj2); // true (same memory address) obj2.name = 'Bob'; console.log(obj1.name); // "Bob" (mutation affects both!) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = obj1; // Same reference console.log(obj1 === obj2); // true (same memory address) obj2.name = 'Bob'; console.log(obj1.name); // "Bob" (mutation affects both!) CODE_BLOCK: const obj1 = { name: 'Alice' }; const obj2 = obj1; // Same reference console.log(obj1 === obj2); // true (same memory address) obj2.name = 'Bob'; console.log(obj1.name); // "Bob" (mutation affects both!) COMMAND_BLOCK: function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; } const MemoizedChild = React.memo(Child); function Parent() { const [count, setCount] = useState(0); const config = { theme: 'dark' }; // New object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; } const MemoizedChild = React.memo(Child); function Parent() { const [count, setCount] = useState(0); const config = { theme: 'dark' }; // New object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; } const MemoizedChild = React.memo(Child); function Parent() { const [count, setCount] = useState(0); const config = { theme: 'dark' }; // New object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const items = [1, 2, 3]; // New array every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedList items={items} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const items = [1, 2, 3]; // New array every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedList items={items} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const items = [1, 2, 3]; // New array every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedList items={items} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = () => { // New function every render! console.log('Clicked'); }; return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = () => { // New function every render! console.log('Clicked'); }; return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = () => { // New function every render! console.log('Clicked'); }; return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme: 'dark' }), []); // Same object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme: 'dark' }), []); // Same object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme: 'dark' }), []); // Same object every render! return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Parent({ theme }) { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme }), [theme]); // Recreate only when 'theme' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent({ theme }) { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme }), [theme]); // Recreate only when 'theme' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Parent({ theme }) { const [count, setCount] = useState(0); const config = useMemo(() => ({ theme }), [theme]); // Recreate only when 'theme' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedChild config={config} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { // Same function every render! console.log('Clicked'); }, []); // No dependencies = function never changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { // Same function every render! console.log('Clicked'); }, []); // No dependencies = function never changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } COMMAND_BLOCK: function Parent() { const [count, setCount] = useState(0); const handleClick = useCallback(() => { // Same function every render! console.log('Clicked'); }, []); // No dependencies = function never changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } COMMAND_BLOCK: const handleClick = useMemo(() => { return () => console.log('Clicked'); }, []); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const handleClick = useMemo(() => { return () => console.log('Clicked'); }, []); COMMAND_BLOCK: const handleClick = useMemo(() => { return () => console.log('Clicked'); }, []); COMMAND_BLOCK: function Parent({ userId }) { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log(`Clicked for user ${userId}`); }, [userId]); // Recreate when 'userId' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent({ userId }) { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log(`Clicked for user ${userId}`); }, [userId]); // Recreate when 'userId' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } COMMAND_BLOCK: function Parent({ userId }) { const [count, setCount] = useState(0); const handleClick = useCallback(() => { console.log(`Clicked for user ${userId}`); }, [userId]); // Recreate when 'userId' changes return ( <div> <button onClick={() => setCount(count + 1)}>Count: {count}</button> <MemoizedButton onClick={handleClick} /> </div> ); } CODE_BLOCK: const Child = React.memo(function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; }); Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const Child = React.memo(function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; }); CODE_BLOCK: const Child = React.memo(function Child({ config }) { console.log('Child rendered'); return <div>{config.theme}</div>; }); COMMAND_BLOCK: // Shallow comparison (default) oldProps.config === newProps.config // Checks reference only // Deep comparison (custom) React.memo(Child, (prevProps, nextProps) => { return prevProps.config.theme === nextProps.config.theme; // Custom logic }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Shallow comparison (default) oldProps.config === newProps.config // Checks reference only // Deep comparison (custom) React.memo(Child, (prevProps, nextProps) => { return prevProps.config.theme === nextProps.config.theme; // Custom logic }); COMMAND_BLOCK: // Shallow comparison (default) oldProps.config === newProps.config // Checks reference only // Deep comparison (custom) React.memo(Child, (prevProps, nextProps) => { return prevProps.config.theme === nextProps.config.theme; // Custom logic }); COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); <MemoizedChild config={config} /> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); <MemoizedChild config={config} /> COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); <MemoizedChild config={config} /> COMMAND_BLOCK: const handleClick = useCallback(() => {}, []); <MemoizedButton onClick={handleClick} /> Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const handleClick = useCallback(() => {}, []); <MemoizedButton onClick={handleClick} /> COMMAND_BLOCK: const handleClick = useCallback(() => {}, []); <MemoizedButton onClick={handleClick} /> COMMAND_BLOCK: const sortedData = useMemo(() => { return data.sort((a, b) => a.value - b.value); // Expensive! }, [data]); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const sortedData = useMemo(() => { return data.sort((a, b) => a.value - b.value); // Expensive! }, [data]); COMMAND_BLOCK: const sortedData = useMemo(() => { return data.sort((a, b) => a.value - b.value); // Expensive! }, [data]); COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); useEffect(() => { fetchData(config); // Won't re-run unnecessarily }, [config]); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); useEffect(() => { fetchData(config); // Won't re-run unnecessarily }, [config]); COMMAND_BLOCK: const config = useMemo(() => ({ theme: 'dark' }), []); useEffect(() => { fetchData(config); // Won't re-run unnecessarily }, [config]); COMMAND_BLOCK: // Unnecessary const count = useMemo(() => 5, []); // Just use the value const count = 5; Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Unnecessary const count = useMemo(() => 5, []); // Just use the value const count = 5; COMMAND_BLOCK: // Unnecessary const count = useMemo(() => 5, []); // Just use the value const count = 5; COMMAND_BLOCK: // Overkill if rendering <div>{text}</div> is cheap const text = useMemo(() => someString, []); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Overkill if rendering <div>{text}</div> is cheap const text = useMemo(() => someString, []); COMMAND_BLOCK: // Overkill if rendering <div>{text}</div> is cheap const text = useMemo(() => someString, []); COMMAND_BLOCK: // No benefit if Child isn't memoized const config = useMemo(() => ({ theme: 'dark' }), []); <Child config={config} /> // Not wrapped in React.memo Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // No benefit if Child isn't memoized const config = useMemo(() => ({ theme: 'dark' }), []); <Child config={config} /> // Not wrapped in React.memo COMMAND_BLOCK: // No benefit if Child isn't memoized const config = useMemo(() => ({ theme: 'dark' }), []); <Child config={config} /> // Not wrapped in React.memo COMMAND_BLOCK: function Component({ userId }) { const handleClick = useCallback(() => { console.log(`User: ${userId}`); // Uses 'userId' }, []); // Missing dependency! return <button onClick={handleClick}>Click</button>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Component({ userId }) { const handleClick = useCallback(() => { console.log(`User: ${userId}`); // Uses 'userId' }, []); // Missing dependency! return <button onClick={handleClick}>Click</button>; } COMMAND_BLOCK: function Component({ userId }) { const handleClick = useCallback(() => { console.log(`User: ${userId}`); // Uses 'userId' }, []); // Missing dependency! return <button onClick={handleClick}>Click</button>; } COMMAND_BLOCK: const handleClick = useCallback(() => { console.log(`User: ${userId}`); }, [userId]); // Include 'userId' Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const handleClick = useCallback(() => { console.log(`User: ${userId}`); }, [userId]); // Include 'userId' COMMAND_BLOCK: const handleClick = useCallback(() => { console.log(`User: ${userId}`); }, [userId]); // Include 'userId' COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); // Useless! return <Child config={config} />; // Child isn't memoized } function Child({ config }) { return <div>{config.theme}</div>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); // Useless! return <Child config={config} />; // Child isn't memoized } function Child({ config }) { return <div>{config.theme}</div>; } COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); // Useless! return <Child config={config} />; // Child isn't memoized } function Child({ config }) { return <div>{config.theme}</div>; } COMMAND_BLOCK: const MemoizedChild = React.memo(Child); // Now useMemo helps function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <MemoizedChild config={config} />; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: const MemoizedChild = React.memo(Child); // Now useMemo helps function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <MemoizedChild config={config} />; } COMMAND_BLOCK: const MemoizedChild = React.memo(Child); // Now useMemo helps function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <MemoizedChild config={config} />; } CODE_BLOCK: function Parent() { return <Child config={{ theme: 'dark' }} />; // New object every render! } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function Parent() { return <Child config={{ theme: 'dark' }} />; // New object every render! } CODE_BLOCK: function Parent() { return <Child config={{ theme: 'dark' }} />; // New object every render! } COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <Child config={config} />; // Stable reference } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <Child config={config} />; // Stable reference } COMMAND_BLOCK: function Parent() { const config = useMemo(() => ({ theme: 'dark' }), []); return <Child config={config} />; // Stable reference } COMMAND_BLOCK: // Too much memoization function Component() { const a = useMemo(() => 1, []); const b = useMemo(() => 2, []); const sum = useMemo(() => a + b, [a, b]); return <div>{sum}</div>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: // Too much memoization function Component() { const a = useMemo(() => 1, []); const b = useMemo(() => 2, []); const sum = useMemo(() => a + b, [a, b]); return <div>{sum}</div>; } COMMAND_BLOCK: // Too much memoization function Component() { const a = useMemo(() => 1, []); const b = useMemo(() => 2, []); const sum = useMemo(() => a + b, [a, b]); return <div>{sum}</div>; } CODE_BLOCK: function Component() { const a = 1; const b = 2; const sum = a + b; // Fast enough without memoization return <div>{sum}</div>; } Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: function Component() { const a = 1; const b = 2; const sum = a + b; // Fast enough without memoization return <div>{sum}</div>; } CODE_BLOCK: function Component() { const a = 1; const b = 2; const sum = a + b; // Fast enough without memoization return <div>{sum}</div>; } COMMAND_BLOCK: function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // New object every render → all consumers re-render! // const value = { theme, setTheme }; // Stable reference const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // New object every render → all consumers re-render! // const value = { theme, setTheme }; // Stable reference const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } COMMAND_BLOCK: function ThemeProvider({ children }) { const [theme, setTheme] = useState('light'); // New object every render → all consumers re-render! // const value = { theme, setTheme }; // Stable reference const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> {children} </ThemeContext.Provider> ); } COMMAND_BLOCK: function UserList({ users, filter }) { const filteredUsers = useMemo(() => { return users.filter(user => { // Expensive filtering logic return user.name.toLowerCase().includes(filter.toLowerCase()); }); }, [users, filter]); // Only recompute when 'users' or 'filter' changes return ( <ul> {filteredUsers.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function UserList({ users, filter }) { const filteredUsers = useMemo(() => { return users.filter(user => { // Expensive filtering logic return user.name.toLowerCase().includes(filter.toLowerCase()); }); }, [users, filter]); // Only recompute when 'users' or 'filter' changes return ( <ul> {filteredUsers.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } COMMAND_BLOCK: function UserList({ users, filter }) { const filteredUsers = useMemo(() => { return users.filter(user => { // Expensive filtering logic return user.name.toLowerCase().includes(filter.toLowerCase()); }); }, [users, filter]); // Only recompute when 'users' or 'filter' changes return ( <ul> {filteredUsers.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } COMMAND_BLOCK: function ItemList({ items, onDelete }) { return ( <ul> {items.map(item => ( <Item key={item.id} item={item} onDelete={onDelete} /> ))} </ul> ); } const Item = React.memo(function Item({ item, onDelete }) { // New function every render // const handleDelete = () => onDelete(item.id); // Memoized const handleDelete = useCallback(() => { onDelete(item.id); }, [item.id, onDelete]); return ( <li> {item.name} <button onClick={handleDelete}>Delete</button> </li> ); }); Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function ItemList({ items, onDelete }) { return ( <ul> {items.map(item => ( <Item key={item.id} item={item} onDelete={onDelete} /> ))} </ul> ); } const Item = React.memo(function Item({ item, onDelete }) { // New function every render // const handleDelete = () => onDelete(item.id); // Memoized const handleDelete = useCallback(() => { onDelete(item.id); }, [item.id, onDelete]); return ( <li> {item.name} <button onClick={handleDelete}>Delete</button> </li> ); }); COMMAND_BLOCK: function ItemList({ items, onDelete }) { return ( <ul> {items.map(item => ( <Item key={item.id} item={item} onDelete={onDelete} /> ))} </ul> ); } const Item = React.memo(function Item({ item, onDelete }) { // New function every render // const handleDelete = () => onDelete(item.id); // Memoized const handleDelete = useCallback(() => { onDelete(item.id); }, [item.id, onDelete]); return ( <li> {item.name} <button onClick={handleDelete}>Delete</button> </li> ); }); COMMAND_BLOCK: function Component() { const configRef = useRef({ theme: 'dark' }); useEffect(() => { // Always uses the same object reference fetchData(configRef.current); }, []); // No dependencies needed return <div>Content</div>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function Component() { const configRef = useRef({ theme: 'dark' }); useEffect(() => { // Always uses the same object reference fetchData(configRef.current); }, []); // No dependencies needed return <div>Content</div>; } COMMAND_BLOCK: function Component() { const configRef = useRef({ theme: 'dark' }); useEffect(() => { // Always uses the same object reference fetchData(configRef.current); }, []); // No dependencies needed return <div>Content</div>; } CODE_BLOCK: const obj1 = { theme: 'dark' }; const obj2 = { theme: 'dark' }; console.log(Object.is(obj1, obj2)); // false (different references) const obj3 = obj1; console.log(Object.is(obj1, obj3)); // true (same reference) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK: const obj1 = { theme: 'dark' }; const obj2 = { theme: 'dark' }; console.log(Object.is(obj1, obj2)); // false (different references) const obj3 = obj1; console.log(Object.is(obj1, obj3)); // true (same reference) CODE_BLOCK: const obj1 = { theme: 'dark' }; const obj2 = { theme: 'dark' }; console.log(Object.is(obj1, obj2)); // false (different references) const obj3 = obj1; console.log(Object.is(obj1, obj3)); // true (same reference) COMMAND_BLOCK: function useWhyDidYouUpdate(name, props) { const previousProps = useRef(); useEffect(() => { if (previousProps.current) { const allKeys = Object.keys({ ...previousProps.current, ...props }); const changedProps = {}; allKeys.forEach(key => { if (previousProps.current[key] !== props[key]) { changedProps[key] = { from: previousProps.current[key], to: props[key] }; } }); if (Object.keys(changedProps).length) { console.log('[why-did-you-update]', name, changedProps); } } previousProps.current = props; }); } function MyComponent(props) { useWhyDidYouUpdate('MyComponent', props); return <div>Content</div>; } Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK: function useWhyDidYouUpdate(name, props) { const previousProps = useRef(); useEffect(() => { if (previousProps.current) { const allKeys = Object.keys({ ...previousProps.current, ...props }); const changedProps = {}; allKeys.forEach(key => { if (previousProps.current[key] !== props[key]) { changedProps[key] = { from: previousProps.current[key], to: props[key] }; } }); if (Object.keys(changedProps).length) { console.log('[why-did-you-update]', name, changedProps); } } previousProps.current = props; }); } function MyComponent(props) { useWhyDidYouUpdate('MyComponent', props); return <div>Content</div>; } COMMAND_BLOCK: function useWhyDidYouUpdate(name, props) { const previousProps = useRef(); useEffect(() => { if (previousProps.current) { const allKeys = Object.keys({ ...previousProps.current, ...props }); const changedProps = {}; allKeys.forEach(key => { if (previousProps.current[key] !== props[key]) { changedProps[key] = { from: previousProps.current[key], to: props[key] }; } }); if (Object.keys(changedProps).length) { console.log('[why-did-you-update]', name, changedProps); } } previousProps.current = props; }); } function MyComponent(props) { useWhyDidYouUpdate('MyComponent', props); return <div>Content</div>; } - Clicking the button triggers Parent to re-render - const config = { theme: 'dark' } creates a new object - React compares the old and new config props: oldConfig === newConfig → false - React thinks the props changed, so it re-renders MemoizedChild - useMemo — Memoize values (objects, arrays, expensive computations) - useCallback — Memoize functions (special case of useMemo) - React.memo — Memoize entire components - useMemo returns the same object ({ theme: 'dark' }) on every render - The object is only recreated if dependencies change (empty array [] = never) - Now oldConfig === newConfig → true, so MemoizedChild doesn't re-render - When count changes, config stays the same (no re-render) - When theme changes, config is recreated (re-render happens, as expected) - React shallowly compares old and new props - If all props are referentially equal, skip re-render - If any prop changes (by reference), re-render - Passing objects/arrays to memoized child components - Passing functions to memoized child components - Expensive computations - Dependencies in useEffect or other hooks - Primitives (already compared by value) - Child doesn't re-render expensively - Component always re-renders anyway - Premature optimization - Open React DevTools - Go to Profiler tab - Start recording - Interact with your app - Look at "Why did this render?" section - "In JavaScript, objects are compared by reference, not value, so {} !== {} even though they look identical" - React's impact: "React uses referential equality to check if props changed. If you create a new object on every render, React thinks the props changed, causing re-renders" - Solution: "useMemo and useCallback stabilize references across renders, preventing unnecessary re-renders in memoized components" - Example: "If you pass config={{ theme: 'dark' }} to a memoized child, it re-renders every time. Using useMemo(() => ({ theme: 'dark' }), []) fixes it" - Best practice: "Only memoize when you have a performance problem — measure first, optimize second"