🎯 useRef: Refs vs Variables Deep Dive
Master React's escape hatch: Direct DOM access, mutable values, and when refs are better than state
🎯 What You'll Learn
- Understanding refs vs regular variables vs state
- Direct DOM manipulation and focus management
- Storing mutable values that don't trigger re-renders
- Ref forwarding and imperative APIs
- Common ref patterns and anti-patterns
- Performance optimizations with refs
- Real-world scenarios and best practices
🎯 Understanding useRef Fundamentals
Refs vs State vs Variables
function RefVsStateVsVariables() {
// 🎯 State: Triggers re-render when changed
const [stateValue, setStateValue] = useState(0);
// 🎯 Ref: Persists between renders, doesn't trigger re-render
const refValue = useRef(0);
// ❌ Regular variable: Gets reset on every render
let regularVariable = 0;
// 🎯 Ref for tracking render count
const renderCount = useRef(0);
// Increment render count on every render
renderCount.current += 1;
const handleStateIncrement = () => {
setStateValue((prev) => prev + 1); // ✅ Triggers re-render
};
const handleRefIncrement = () => {
refValue.current += 1; // ✅ Persists but no re-render
console.log("Ref value:", refValue.current);
};
const handleVariableIncrement = () => {
regularVariable += 1; // ❌ Will be reset on next render
console.log("Variable value:", regularVariable);
};
const forceRerender = () => {
setStateValue((prev) => prev); // Force re-render without changing state
};
return (
<div className="ref-comparison">
<h3>Refs vs State vs Variables</h3>
<div className="render-info">
<p>
<strong>Render count:</strong> {renderCount.current}
</p>
</div>
<div className="value-display">
<div className="state-section">
<h4>State Value (triggers re-render)</h4>
<p>Current value: {stateValue}</p>
<button onClick={handleStateIncrement}>Increment State</button>
</div>
<div className="ref-section">
<h4>Ref Value (persists, no re-render)</h4>
<p>Current value: {refValue.current}</p>
<button onClick={handleRefIncrement}>Increment Ref</button>
<p>
<em>Check console for actual value</em>
</p>
</div>
<div className="variable-section">
<h4>Regular Variable (resets each render)</h4>
<p>Current value: {regularVariable}</p>
<button onClick={handleVariableIncrement}>Increment Variable</button>
<p>
<em>Always resets to 0</em>
</p>
</div>
</div>
<div className="controls">
<button onClick={forceRerender}>Force Re-render</button>
<button
onClick={() => {
console.log("Current values:");
console.log("State:", stateValue);
console.log("Ref:", refValue.current);
console.log("Variable:", regularVariable);
}}
>
Log All Values
</button>
</div>
<div className="explanation">
<h4>Key Differences:</h4>
<ul>
<li>
<strong>State:</strong> Triggers re-render, UI updates automatically
</li>
<li>
<strong>Ref:</strong> Persists between renders, no re-render, manual
UI updates
</li>
<li>
<strong>Variable:</strong> Resets on every render, loses value
</li>
</ul>
</div>
</div>
);
}
DOM Refs and Element Access
function DOMRefExamples() {
// 🎯 Refs for DOM elements
const inputRef = useRef(null);
const textareaRef = useRef(null);
const divRef = useRef(null);
const videoRef = useRef(null);
// 🎯 Refs for storing DOM measurements
const measurementsRef = useRef({ width: 0, height: 0 });
const [measurements, setMeasurements] = useState({ width: 0, height: 0 });
const [inputValue, setInputValue] = useState("");
const [isPlaying, setIsPlaying] = useState(false);
// 🎯 Focus management
const focusInput = () => {
if (inputRef.current) {
inputRef.current.focus();
inputRef.current.select(); // Select all text
}
};
const clearAndFocus = () => {
setInputValue("");
// Focus after state update
setTimeout(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, 0);
};
// 🎯 Scroll management
const scrollToTop = () => {
if (divRef.current) {
divRef.current.scrollTop = 0;
}
};
const scrollToBottom = () => {
if (divRef.current) {
divRef.current.scrollTop = divRef.current.scrollHeight;
}
};
// 🎯 Measurements
const measureElement = () => {
if (divRef.current) {
const rect = divRef.current.getBoundingClientRect();
const newMeasurements = {
width: rect.width,
height: rect.height,
top: rect.top,
left: rect.left,
};
// Store in ref (doesn't trigger re-render)
measurementsRef.current = newMeasurements;
// Update state to show in UI
setMeasurements(newMeasurements);
}
};
// 🎯 Video control
const toggleVideo = () => {
if (videoRef.current) {
if (isPlaying) {
videoRef.current.pause();
} else {
videoRef.current.play();
}
setIsPlaying(!isPlaying);
}
};
const setVideoTime = (seconds) => {
if (videoRef.current) {
videoRef.current.currentTime = seconds;
}
};
// 🎯 Text manipulation
const insertTextAtCursor = (text) => {
if (textareaRef.current) {
const textarea = textareaRef.current;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const currentValue = textarea.value;
const newValue =
currentValue.substring(0, start) + text + currentValue.substring(end);
textarea.value = newValue;
// Set cursor position after inserted text
const newCursorPos = start + text.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
textarea.focus();
}
};
return (
<div className="dom-ref-examples">
<h3>DOM Refs and Element Access</h3>
{/* Input Focus Management */}
<div className="input-section">
<h4>Input Focus Management</h4>
<input
ref={inputRef}
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type something..."
/>
<div className="input-controls">
<button onClick={focusInput}>Focus & Select</button>
<button onClick={clearAndFocus}>Clear & Focus</button>
<button
onClick={() => {
if (inputRef.current) {
inputRef.current.blur();
}
}}
>
Blur
</button>
</div>
</div>
{/* Textarea with Text Insertion */}
<div className="textarea-section">
<h4>Textarea Manipulation</h4>
<textarea
ref={textareaRef}
rows={4}
cols={50}
placeholder="Click buttons to insert text at cursor..."
/>
<div className="textarea-controls">
<button onClick={() => insertTextAtCursor("Hello ")}>
Insert "Hello "
</button>
<button onClick={() => insertTextAtCursor("World!")}>
Insert "World!"
</button>
<button onClick={() => insertTextAtCursor("\n")}>
Insert New Line
</button>
<button
onClick={() => {
if (textareaRef.current) {
textareaRef.current.value = "";
textareaRef.current.focus();
}
}}
>
Clear
</button>
</div>
</div>
{/* Scrollable Div */}
<div className="scroll-section">
<h4>Scroll Management</h4>
<div
ref={divRef}
className="scrollable-div"
style={{
height: "150px",
overflow: "auto",
border: "1px solid #ccc",
padding: "10px",
}}
>
{Array.from({ length: 50 }, (_, i) => (
<p key={i}>Line {i + 1} - This is some scrollable content</p>
))}
</div>
<div className="scroll-controls">
<button onClick={scrollToTop}>Scroll to Top</button>
<button onClick={scrollToBottom}>Scroll to Bottom</button>
<button onClick={measureElement}>Measure Element</button>
</div>
</div>
{/* Measurements Display */}
<div className="measurements-section">
<h4>Element Measurements</h4>
<pre>{JSON.stringify(measurements, null, 2)}</pre>
</div>
{/* Video Control */}
<div className="video-section">
<h4>Video Control</h4>
<video
ref={videoRef}
width="300"
height="200"
controls={false}
style={{ border: "1px solid #ccc" }}
>
<source src="/sample-video.mp4" type="video/mp4" />
Your browser does not support the video tag.
</video>
<div className="video-controls">
<button onClick={toggleVideo}>{isPlaying ? "Pause" : "Play"}</button>
<button onClick={() => setVideoTime(0)}>Reset</button>
<button onClick={() => setVideoTime(10)}>Skip to 10s</button>
</div>
</div>
</div>
);
}
🔄 Mutable Values and Performance
Using Refs for Mutable Values
function MutableValueExamples() {
const [count, setCount] = useState(0);
const [messages, setMessages] = useState([]);
// 🎯 Refs for values that don't need to trigger re-renders
const timerIdRef = useRef(null);
const previousCountRef = useRef(0);
const callbackCountRef = useRef(0);
const isComponentMountedRef = useRef(true);
// 🎯 Ref for storing expensive calculations
const expensiveValueRef = useRef(null);
const lastCalculationInputRef = useRef(null);
// 🎯 Track previous value
useEffect(() => {
previousCountRef.current = count;
});
// 🎯 Cleanup on unmount
useEffect(() => {
return () => {
isComponentMountedRef.current = false;
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
};
}, []);
const startTimer = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
}
timerIdRef.current = setInterval(() => {
// ✅ Check if component is still mounted
if (isComponentMountedRef.current) {
setCount((prevCount) => prevCount + 1);
}
}, 1000);
};
const stopTimer = () => {
if (timerIdRef.current) {
clearInterval(timerIdRef.current);
timerIdRef.current = null;
}
};
// 🎯 Expensive calculation with caching
const getExpensiveValue = (input) => {
// Only recalculate if input changed
if (lastCalculationInputRef.current !== input) {
console.log("Performing expensive calculation...");
// Simulate expensive operation
let result = 0;
for (let i = 0; i < input * 1000000; i++) {
result += Math.random();
}
expensiveValueRef.current = result;
lastCalculationInputRef.current = input;
}
return expensiveValueRef.current;
};
// 🎯 Callback with ref to avoid stale closures
const handleAsyncOperation = useCallback(() => {
callbackCountRef.current += 1;
const currentCallCount = callbackCountRef.current;
console.log(`Starting async operation #${currentCallCount}`);
setTimeout(() => {
// ✅ Check if this is still the latest call
if (
callbackCountRef.current === currentCallCount &&
isComponentMountedRef.current
) {
setMessages((prev) => [
...prev,
{
id: Date.now(),
text: `Async operation #${currentCallCount} completed`,
timestamp: new Date().toLocaleTimeString(),
},
]);
} else {
console.log(`Async operation #${currentCallCount} was superseded`);
}
}, 2000);
}, []);
const clearMessages = () => {
setMessages([]);
};
const expensiveValue = getExpensiveValue(count);
return (
<div className="mutable-value-examples">
<h3>Mutable Values with Refs</h3>
<div className="counter-section">
<h4>Timer with Ref Management</h4>
<p>Current count: {count}</p>
<p>Previous count: {previousCountRef.current}</p>
<p>Change: {count - previousCountRef.current}</p>
<div className="timer-controls">
<button onClick={startTimer}>Start Timer</button>
<button onClick={stopTimer}>Stop Timer</button>
<button onClick={() => setCount(0)}>Reset Count</button>
</div>
</div>
<div className="expensive-calculation">
<h4>Cached Expensive Calculation</h4>
<p>Input: {count}</p>
<p>
Expensive result: {expensiveValue?.toFixed(2) || "Not calculated"}
</p>
<p>
<em>Check console - calculation only runs when count changes</em>
</p>
</div>
<div className="async-section">
<h4>Async Operations with Race Condition Prevention</h4>
<button onClick={handleAsyncOperation}>
Start Async Operation (2s delay)
</button>
<p>Callback count: {callbackCountRef.current}</p>
<div className="messages">
<h5>Messages:</h5>
{messages.length === 0 ? (
<p>No messages yet</p>
) : (
<ul>
{messages.map((message) => (
<li key={message.id}>
<strong>{message.timestamp}:</strong> {message.text}
</li>
))}
</ul>
)}
{messages.length > 0 && (
<button onClick={clearMessages}>Clear Messages</button>
)}
</div>
</div>
<div className="ref-info">
<h4>Ref Usage Summary:</h4>
<ul>
<li>
<code>timerIdRef</code>: Stores timer ID for cleanup
</li>
<li>
<code>previousCountRef</code>: Tracks previous state value
</li>
<li>
<code>callbackCountRef</code>: Prevents race conditions
</li>
<li>
<code>isComponentMountedRef</code>: Prevents state updates after
unmount
</li>
<li>
<code>expensiveValueRef</code>: Caches expensive calculations
</li>
</ul>
</div>
</div>
);
}
Performance Optimization with Refs
function PerformanceOptimizationWithRefs() {
const [items, setItems] = useState(
Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
selected: false,
}))
);
const [filter, setFilter] = useState("");
// 🎯 Refs for performance optimization
const selectedItemsRef = useRef(new Set());
const lastFilterRef = useRef("");
const filteredItemsRef = useRef([]);
const renderCountRef = useRef(0);
renderCountRef.current += 1;
// 🎯 Memoized filtered items with ref caching
const filteredItems = useMemo(() => {
// Only recalculate if filter actually changed
if (
lastFilterRef.current === filter &&
filteredItemsRef.current.length > 0
) {
return filteredItemsRef.current;
}
console.log("Filtering items...");
const result = items.filter((item) =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
lastFilterRef.current = filter;
filteredItemsRef.current = result;
return result;
}, [items, filter]);
// 🎯 Optimized selection handling
const toggleItemSelection = useCallback((itemId) => {
const selectedItems = selectedItemsRef.current;
if (selectedItems.has(itemId)) {
selectedItems.delete(itemId);
} else {
selectedItems.add(itemId);
}
// Update items state
setItems((prevItems) =>
prevItems.map((item) =>
item.id === itemId
? { ...item, selected: selectedItems.has(itemId) }
: item
)
);
}, []);
const selectAll = () => {
const selectedItems = selectedItemsRef.current;
selectedItems.clear();
filteredItems.forEach((item) => {
selectedItems.add(item.id);
});
setItems((prevItems) =>
prevItems.map((item) => ({
...item,
selected: selectedItems.has(item.id),
}))
);
};
const clearSelection = () => {
selectedItemsRef.current.clear();
setItems((prevItems) =>
prevItems.map((item) => ({ ...item, selected: false }))
);
};
const getSelectionCount = () => {
return selectedItemsRef.current.size;
};
return (
<div className="performance-optimization">
<h3>Performance Optimization with Refs</h3>
<div className="stats">
<p>Render count: {renderCountRef.current}</p>
<p>Total items: {items.length}</p>
<p>Filtered items: {filteredItems.length}</p>
<p>Selected items: {getSelectionCount()}</p>
</div>
<div className="controls">
<input
type="text"
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
<div className="selection-controls">
<button onClick={selectAll}>Select All Filtered</button>
<button onClick={clearSelection}>Clear Selection</button>
</div>
</div>
<div className="items-list" style={{ height: "300px", overflow: "auto" }}>
{filteredItems.map((item) => (
<div
key={item.id}
className={`item ${item.selected ? "selected" : ""}`}
onClick={() => toggleItemSelection(item.id)}
style={{
padding: "5px",
cursor: "pointer",
backgroundColor: item.selected ? "#e3f2fd" : "transparent",
}}
>
<input
type="checkbox"
checked={item.selected}
onChange={() => {}} // Handled by parent click
/>
{item.name}
</div>
))}
</div>
<div className="optimization-notes">
<h4>Optimization Techniques Used:</h4>
<ul>
<li>
<strong>selectedItemsRef:</strong> Tracks selection without
triggering re-renders
</li>
<li>
<strong>filteredItemsRef:</strong> Caches filtered results
</li>
<li>
<strong>lastFilterRef:</strong> Prevents unnecessary filtering
</li>
<li>
<strong>useMemo:</strong> Memoizes expensive filtering operation
</li>
<li>
<strong>useCallback:</strong> Prevents function recreation
</li>
</ul>
</div>
</div>
);
}
🔄 Ref Forwarding and Imperative APIs
Ref Forwarding Patterns
// 🎯 Basic ref forwarding
const CustomInput = forwardRef((props, ref) => {
const { label, error, ...inputProps } = props;
return (
<div className="custom-input">
{label && <label>{label}</label>}
<input ref={ref} {...inputProps} />
{error && <span className="error">{error}</span>}
</div>
);
});
// 🎯 Advanced ref forwarding with imperative API
const AdvancedInput = forwardRef((props, ref) => {
const { onValueChange, ...otherProps } = props;
const inputRef = useRef(null);
const [value, setValue] = useState("");
// 🎯 Expose imperative API
useImperativeHandle(
ref,
() => ({
focus: () => {
inputRef.current?.focus();
},
blur: () => {
inputRef.current?.blur();
},
clear: () => {
setValue("");
inputRef.current?.focus();
},
getValue: () => value,
setValue: (newValue) => {
setValue(newValue);
},
selectAll: () => {
inputRef.current?.select();
},
insertAtCursor: (text) => {
const input = inputRef.current;
if (input) {
const start = input.selectionStart;
const end = input.selectionEnd;
const newValue =
value.substring(0, start) + text + value.substring(end);
setValue(newValue);
// Set cursor position after inserted text
setTimeout(() => {
const newPos = start + text.length;
input.setSelectionRange(newPos, newPos);
}, 0);
}
},
}),
[value]
);
const handleChange = (e) => {
const newValue = e.target.value;
setValue(newValue);
onValueChange?.(newValue);
};
return (
<input
ref={inputRef}
value={value}
onChange={handleChange}
{...otherProps}
/>
);
});
// 🎯 Modal component with ref forwarding
const Modal = forwardRef(({ children, title, onClose }, ref) => {
const [isOpen, setIsOpen] = useState(false);
const modalRef = useRef(null);
const previousFocusRef = useRef(null);
useImperativeHandle(
ref,
() => ({
open: () => {
previousFocusRef.current = document.activeElement;
setIsOpen(true);
},
close: () => {
setIsOpen(false);
previousFocusRef.current?.focus();
},
isOpen: () => isOpen,
}),
[isOpen]
);
// 🎯 Focus management
useEffect(() => {
if (isOpen && modalRef.current) {
modalRef.current.focus();
}
}, [isOpen]);
// 🎯 Escape key handling
useEffect(() => {
const handleEscape = (e) => {
if (e.key === "Escape" && isOpen) {
setIsOpen(false);
onClose?.();
}
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
className="modal-overlay"
onClick={() => {
setIsOpen(false);
onClose?.();
}}
>
<div
ref={modalRef}
className="modal-content"
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
>
<div className="modal-header">
<h3>{title}</h3>
<button
onClick={() => {
setIsOpen(false);
onClose?.();
}}
>
×
</button>
</div>
<div className="modal-body">{children}</div>
</div>
</div>
);
});
// Usage examples
function RefForwardingExamples() {
const customInputRef = useRef(null);
const advancedInputRef = useRef(null);
const modalRef = useRef(null);
const [inputValue, setInputValue] = useState("");
return (
<div className="ref-forwarding-examples">
<h3>Ref Forwarding Examples</h3>
{/* Basic ref forwarding */}
<div className="basic-forwarding">
<h4>Basic Ref Forwarding</h4>
<CustomInput
ref={customInputRef}
label="Custom Input"
placeholder="Type something..."
/>
<button onClick={() => customInputRef.current?.focus()}>
Focus Custom Input
</button>
</div>
{/* Advanced ref forwarding with imperative API */}
<div className="advanced-forwarding">
<h4>Advanced Ref Forwarding (Imperative API)</h4>
<AdvancedInput
ref={advancedInputRef}
placeholder="Advanced input..."
onValueChange={setInputValue}
/>
<p>Current value: {inputValue}</p>
<div className="advanced-controls">
<button onClick={() => advancedInputRef.current?.focus()}>
Focus
</button>
<button onClick={() => advancedInputRef.current?.clear()}>
Clear
</button>
<button onClick={() => advancedInputRef.current?.selectAll()}>
Select All
</button>
<button
onClick={() =>
advancedInputRef.current?.insertAtCursor(" [INSERTED] ")
}
>
Insert Text
</button>
<button
onClick={() => {
const value = advancedInputRef.current?.getValue();
alert(`Current value: ${value}`);
}}
>
Get Value
</button>
</div>
</div>
{/* Modal with ref forwarding */}
<div className="modal-forwarding">
<h4>Modal with Ref Forwarding</h4>
<button onClick={() => modalRef.current?.open()}>Open Modal</button>
<Modal
ref={modalRef}
title="Example Modal"
onClose={() => console.log("Modal closed")}
>
<p>This is modal content.</p>
<p>Press Escape or click outside to close.</p>
<input type="text" placeholder="Focus is managed automatically" />
</Modal>
</div>
</div>
);
}
Custom Hooks with Refs
// 🎯 Custom hook for element visibility
function useIntersectionObserver(options = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const [entry, setEntry] = useState(null);
const elementRef = useRef(null);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
setEntry(entry);
}, options);
observer.observe(element);
return () => {
observer.unobserve(element);
};
}, [options]);
return [elementRef, isIntersecting, entry];
}
// 🎯 Custom hook for click outside
function useClickOutside(callback) {
const ref = useRef(null);
useEffect(() => {
const handleClick = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
callback();
}
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [callback]);
return ref;
}
// 🎯 Custom hook for hover
function useHover() {
const [isHovered, setIsHovered] = useState(false);
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const handleMouseEnter = () => setIsHovered(true);
const handleMouseLeave = () => setIsHovered(false);
element.addEventListener("mouseenter", handleMouseEnter);
element.addEventListener("mouseleave", handleMouseLeave);
return () => {
element.removeEventListener("mouseenter", handleMouseEnter);
element.removeEventListener("mouseleave", handleMouseLeave);
};
}, []);
return [ref, isHovered];
}
// 🎯 Custom hook for element size
function useElementSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
const ref = useRef(null);
useEffect(() => {
const element = ref.current;
if (!element) return;
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
setSize({ width, height });
}
});
resizeObserver.observe(element);
return () => {
resizeObserver.unobserve(element);
};
}, []);
return [ref, size];
}
// Usage examples
function CustomHookExamples() {
const [visibilityRef, isVisible] = useIntersectionObserver({
threshold: 0.5,
});
const [hoverRef, isHovered] = useHover();
const [sizeRef, size] = useElementSize();
const [showDropdown, setShowDropdown] = useState(false);
const dropdownRef = useClickOutside(() => setShowDropdown(false));
return (
<div className="custom-hook-examples">
<h3>Custom Hooks with Refs</h3>
{/* Intersection Observer */}
<div
style={{ height: "200px", overflow: "auto", border: "1px solid #ccc" }}
>
<div style={{ height: "300px", padding: "20px" }}>
<p>Scroll down to see the observed element...</p>
</div>
<div
ref={visibilityRef}
style={{
padding: "20px",
backgroundColor: isVisible ? "lightgreen" : "lightcoral",
transition: "background-color 0.3s",
}}
>
<p>This element is {isVisible ? "visible" : "not visible"}</p>
</div>
<div style={{ height: "300px", padding: "20px" }}>
<p>More content below...</p>
</div>
</div>
{/* Hover Detection */}
<div
ref={hoverRef}
style={{
padding: "20px",
margin: "20px 0",
backgroundColor: isHovered ? "lightblue" : "lightgray",
transition: "background-color 0.3s",
cursor: "pointer",
}}
>
<p>Hover over me! Currently {isHovered ? "hovered" : "not hovered"}</p>
</div>
{/* Element Size */}
<div
ref={sizeRef}
style={{
padding: "20px",
margin: "20px 0",
border: "2px solid #333",
resize: "both",
overflow: "auto",
minWidth: "200px",
minHeight: "100px",
}}
>
<p>Resize me!</p>
<p>Width: {size.width.toFixed(0)}px</p>
<p>Height: {size.height.toFixed(0)}px</p>
</div>
{/* Click Outside */}
<div className="dropdown-container">
<button onClick={() => setShowDropdown(!showDropdown)}>
Toggle Dropdown
</button>
{showDropdown && (
<div
ref={dropdownRef}
style={{
position: "absolute",
top: "100%",
left: 0,
backgroundColor: "white",
border: "1px solid #ccc",
padding: "10px",
zIndex: 1000,
}}
>
<p>Click outside to close</p>
<button onClick={() => alert("Button clicked!")}>Action</button>
</div>
)}
</div>
</div>
);
}
⚠️ Common Mistakes & Anti-Patterns
1. Using Refs Instead of State
// ❌ WRONG: Using ref for UI state
function WrongRefUsage() {
const countRef = useRef(0);
const increment = () => {
countRef.current += 1;
// ❌ UI won't update because no re-render is triggered
};
return (
<div>
<p>Count: {countRef.current}</p> {/* ❌ Won't update */}
<button onClick={increment}>Increment</button>
</div>
);
}
// ✅ CORRECT: Use state for UI values
function CorrectStateUsage() {
const [count, setCount] = useState(0);
const increment = () => {
setCount((prev) => prev + 1); // ✅ Triggers re-render
};
return (
<div>
<p>Count: {count}</p> {/* ✅ Updates correctly */}
<button onClick={increment}>Increment</button>
</div>
);
}
2. Accessing Refs During Render
// ❌ WRONG: Accessing ref.current during render
function WrongRefAccess() {
const inputRef = useRef(null);
// ❌ Don't access ref.current during render
const inputValue = inputRef.current?.value || "";
return (
<div>
<input ref={inputRef} />
<p>Value: {inputValue}</p> {/* ❌ May be stale or null */}
</div>
);
}
// ✅ CORRECT: Access refs in effects or event handlers
function CorrectRefAccess() {
const inputRef = useRef(null);
const [inputValue, setInputValue] = useState("");
const handleGetValue = () => {
// ✅ Access ref in event handler
const value = inputRef.current?.value || "";
setInputValue(value);
};
return (
<div>
<input ref={inputRef} />
<button onClick={handleGetValue}>Get Value</button>
<p>Value: {inputValue}</p>
</div>
);
}
3. Not Handling Null Refs
// ❌ WRONG: Not checking for null
function UnsafeRefAccess() {
const elementRef = useRef(null);
const handleClick = () => {
elementRef.current.focus(); // ❌ May throw error if null
};
return (
<div>
<input ref={elementRef} />
<button onClick={handleClick}>Focus</button>
</div>
);
}
// ✅ CORRECT: Always check for null
function SafeRefAccess() {
const elementRef = useRef(null);
const handleClick = () => {
if (elementRef.current) {
// ✅ Safe null check
elementRef.current.focus();
}
// Or use optional chaining
elementRef.current?.focus(); // ✅ Also safe
};
return (
<div>
<input ref={elementRef} />
<button onClick={handleClick}>Focus</button>
</div>
);
}
4. Ref Callback Mistakes
// ❌ WRONG: Inline ref callback
function InlineRefCallback() {
const [elements, setElements] = useState([]);
return (
<div>
{[1, 2, 3].map((num) => (
<input
key={num}
ref={(el) => {
// ❌ Creates new function on every render
if (el) {
setElements((prev) => [...prev, el]);
}
}}
/>
))}
</div>
);
}
// ✅ CORRECT: Stable ref callback
function StableRefCallback() {
const [elements, setElements] = useState([]);
const elementsRef = useRef([]);
const setElementRef = useCallback(
(index) => (el) => {
if (el) {
elementsRef.current[index] = el;
}
},
[]
);
return (
<div>
{[1, 2, 3].map((num, index) => (
<input
key={num}
ref={setElementRef(index)} // ✅ Stable callback
/>
))}
</div>
);
}
🧪 Mini Challenges
Challenge 1: Focus Management System
Build a focus management system that:
- Tracks focus history
- Provides focus navigation (next/previous)
- Handles focus trapping in modals
- Restores focus when components unmount
💡 Solution
function useFocusManager() {
const focusHistoryRef = useRef([]);
const currentFocusIndexRef = useRef(-1);
const trapContainerRef = useRef(null);
const isTrapActiveRef = useRef(false);
const addToHistory = useCallback((element) => {
if (
element &&
element !== focusHistoryRef.current[currentFocusIndexRef.current]
) {
focusHistoryRef.current.push(element);
currentFocusIndexRef.current = focusHistoryRef.current.length - 1;
}
}, []);
const focusPrevious = useCallback(() => {
if (currentFocusIndexRef.current > 0) {
currentFocusIndexRef.current -= 1;
const element = focusHistoryRef.current[currentFocusIndexRef.current];
if (element && document.contains(element)) {
element.focus();
}
}
}, []);
const focusNext = useCallback(() => {
if (currentFocusIndexRef.current < focusHistoryRef.current.length - 1) {
currentFocusIndexRef.current += 1;
const element = focusHistoryRef.current[currentFocusIndexRef.current];
if (element && document.contains(element)) {
element.focus();
}
}
}, []);
const enableFocusTrap = useCallback((container) => {
trapContainerRef.current = container;
isTrapActiveRef.current = true;
const handleKeyDown = (e) => {
if (
e.key === "Tab" &&
isTrapActiveRef.current &&
trapContainerRef.current
) {
const focusableElements = trapContainerRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
}
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
isTrapActiveRef.current = false;
};
}, []);
const disableFocusTrap = useCallback(() => {
isTrapActiveRef.current = false;
trapContainerRef.current = null;
}, []);
useEffect(() => {
const handleFocus = (e) => {
if (!isTrapActiveRef.current) {
addToHistory(e.target);
}
};
document.addEventListener("focusin", handleFocus);
return () => {
document.removeEventListener("focusin", handleFocus);
};
}, [addToHistory]);
return {
focusPrevious,
focusNext,
enableFocusTrap,
disableFocusTrap,
getFocusHistory: () => focusHistoryRef.current,
};
}
function FocusManagementDemo() {
const { focusPrevious, focusNext, enableFocusTrap, disableFocusTrap } =
useFocusManager();
const [showModal, setShowModal] = useState(false);
const modalRef = useRef(null);
useEffect(() => {
if (showModal && modalRef.current) {
const cleanup = enableFocusTrap(modalRef.current);
return cleanup;
} else {
disableFocusTrap();
}
}, [showModal, enableFocusTrap, disableFocusTrap]);
return (
<div className="focus-management-demo">
<h3>Focus Management System</h3>
<div className="controls">
<button onClick={focusPrevious}>Focus Previous</button>
<button onClick={focusNext}>Focus Next</button>
<button onClick={() => setShowModal(true)}>Open Modal</button>
</div>
<div className="form">
<input placeholder="Input 1" />
<input placeholder="Input 2" />
<button>Button 1</button>
<textarea placeholder="Textarea"></textarea>
<button>Button 2</button>
</div>
{showModal && (
<div className="modal-overlay">
<div ref={modalRef} className="modal" tabIndex={-1}>
<h4>Modal with Focus Trap</h4>
<input placeholder="Modal input 1" />
<input placeholder="Modal input 2" />
<button onClick={() => setShowModal(false)}>Close</button>
<button>Another Button</button>
</div>
</div>
)}
</div>
);
}
Challenge 2: Virtual Scrolling with Refs
Create a virtual scrolling component that:
- Only renders visible items
- Uses refs for scroll position tracking
- Handles dynamic item heights
- Maintains scroll position during updates
💡 Solution
function useVirtualScroll({
items,
itemHeight,
containerHeight,
overscan = 5,
}) {
const scrollElementRef = useRef(null);
const [scrollTop, setScrollTop] = useState(0);
const [isScrolling, setIsScrolling] = useState(false);
const scrollTimeoutRef = useRef(null);
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + containerHeight) / itemHeight) + overscan
);
const visibleItems = items
.slice(startIndex, endIndex + 1)
.map((item, index) => ({
...item,
index: startIndex + index,
}));
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
const handleScroll = useCallback((e) => {
setScrollTop(e.target.scrollTop);
setIsScrolling(true);
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current);
}
scrollTimeoutRef.current = setTimeout(() => {
setIsScrolling(false);
}, 150);
}, []);
const scrollToIndex = useCallback(
(index) => {
if (scrollElementRef.current) {
const scrollTop = index * itemHeight;
scrollElementRef.current.scrollTop = scrollTop;
setScrollTop(scrollTop);
}
},
[itemHeight]
);
const scrollToTop = useCallback(() => {
scrollToIndex(0);
}, [scrollToIndex]);
const scrollToBottom = useCallback(() => {
scrollToIndex(items.length - 1);
}, [scrollToIndex, items.length]);
return {
scrollElementRef,
visibleItems,
totalHeight,
offsetY,
handleScroll,
scrollToIndex,
scrollToTop,
scrollToBottom,
isScrolling,
};
}
function VirtualScrollDemo() {
const items = useMemo(
() =>
Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
description: `Description for item ${i}`,
})),
[]
);
const {
scrollElementRef,
visibleItems,
totalHeight,
offsetY,
handleScroll,
scrollToIndex,
scrollToTop,
scrollToBottom,
isScrolling,
} = useVirtualScroll({
items,
itemHeight: 50,
containerHeight: 400,
});
const [jumpToIndex, setJumpToIndex] = useState("");
const handleJumpTo = () => {
const index = parseInt(jumpToIndex);
if (!isNaN(index) && index >= 0 && index < items.length) {
scrollToIndex(index);
}
};
return (
<div className="virtual-scroll-demo">
<h3>Virtual Scrolling with Refs</h3>
<div className="controls">
<button onClick={scrollToTop}>Scroll to Top</button>
<button onClick={scrollToBottom}>Scroll to Bottom</button>
<div className="jump-controls">
<input
type="number"
value={jumpToIndex}
onChange={(e) => setJumpToIndex(e.target.value)}
placeholder="Jump to index"
min="0"
max={items.length - 1}
/>
<button onClick={handleJumpTo}>Jump</button>
</div>
</div>
<div className="scroll-info">
<p>Total items: {items.length}</p>
<p>Visible items: {visibleItems.length}</p>
<p>Scrolling: {isScrolling ? "Yes" : "No"}</p>
</div>
<div
ref={scrollElementRef}
className="virtual-scroll-container"
style={{
height: "400px",
overflow: "auto",
border: "1px solid #ccc",
}}
onScroll={handleScroll}
>
<div style={{ height: totalHeight, position: "relative" }}>
<div
style={{
transform: `translateY(${offsetY}px)`,
position: "absolute",
top: 0,
left: 0,
right: 0,
}}
>
{visibleItems.map((item) => (
<div
key={item.id}
style={{
height: "50px",
padding: "10px",
borderBottom: "1px solid #eee",
display: "flex",
alignItems: "center",
}}
>
<div>
<strong>{item.name}</strong>
<br />
<small>{item.description}</small>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}
🎯 When and Why: useRef Decision Framework
Quick Decision Tree
🤔 Should I use useRef?
├── Do I need to access DOM elements directly?
│ ├── Yes → useRef for DOM reference ✅
│ └── No → Continue...
│
├── Do I need a value that persists but doesn't trigger re-renders?
│ ├── Yes → useRef for mutable value ✅
│ └── No → Continue...
│
├── Do I need to store timer IDs, intervals, or subscriptions?
│ ├── Yes → useRef for cleanup references ✅
│ └── No → Continue...
│
├── Do I need to track previous values?
│ ├── Yes → useRef for previous state ✅
│ └── No → Continue...
│
├── Do I need to prevent stale closures in effects?
│ ├── Yes → useRef for current values ✅
│ └── No → Use useState instead
│
└── Is this for UI state that should trigger re-renders?
└── Yes → Use useState, not useRef ❌
🎤 Interview Insights
Common Interview Questions
-
"What's the difference between useRef and useState?"
- useRef: Mutable, doesn't trigger re-renders, persists between renders
- useState: Immutable updates, triggers re-renders, for UI state
- Show examples of when to use each
-
"How do you access DOM elements in React?"
- Use useRef to create a ref
- Attach ref to JSX element
- Access via ref.current in effects or event handlers
- Show focus management example
-
"What is ref forwarding and when do you use it?"
- Passing refs through components to child elements
- Use forwardRef and useImperativeHandle
- Common in reusable component libraries
- Show custom input component example
-
"How do you prevent memory leaks with refs?"
- Store cleanup functions (timers, subscriptions)
- Check if component is mounted before state updates
- Clean up in useEffect return function
- Show timer cleanup example
Code Review Red Flags
// 🚨 Red Flags in Interviews:
// ❌ Using ref for UI state
const countRef = useRef(0);
return <p>{countRef.current}</p>; // Won't update
// ❌ Accessing ref during render
const value = inputRef.current?.value; // May be null/stale
// ❌ Not checking for null
elementRef.current.focus(); // May throw error
// ❌ Inline ref callbacks
ref={(el) => { /* new function every render */ }}
// ❌ Forgetting cleanup
const timer = setInterval(...);
// No cleanup in useEffect return
🎯 Key Takeaways
Mental Model
// 🧠 Think of useRef as:
// "A box that holds a value that:
// - Persists between renders
// - Doesn't trigger re-renders when changed
// - Can hold any mutable value
// - Is perfect for DOM access and cleanup"
const ref = useRef(initialValue);
// ref.current = the actual value
// ref itself never changes
Best Practices Summary
- Use refs for DOM access - focus, scroll, measurements
- Use refs for mutable values - timers, counters, flags
- Always check for null - refs may be null initially
- Don't access refs during render - use effects or event handlers
- Use ref forwarding for reusable components
- Store cleanup references - prevent memory leaks
- Prefer useState for UI state - don't use refs for visible data
Next up: useMemo: What, When, and Why - Master performance optimization and expensive calculation caching.
Previous: useEffect Dependencies & Cleanup
💡 Pro tip: In interviews, demonstrate understanding of when NOT to use refs. Show that you know the difference between mutable refs and immutable state, and always explain your choice.# 🚀 useMemo: What, When, and Why
Master React's performance optimization: Expensive calculations, object stability, and when memoization actually helps
🎯 What You'll Learn
- Understanding useMemo fundamentals and when it's needed
- Expensive calculation optimization patterns
- Object and array reference stability
- Dependency array best practices
- Performance measurement and profiling
- Common memoization mistakes and anti-patterns
- Real-world optimization scenarios
- Interview insights and best practices
🎯 Understanding useMemo Fundamentals
What is useMemo?
// 🎯 useMemo caches the result of expensive calculations
const memoizedValue = useMemo(() => {
// Expensive calculation here
return expensiveCalculation(dependencies);
}, [dependencies]);
// 🎯 Basic syntax breakdown:
// useMemo(calculateValue, dependencies)
// - calculateValue: Function that returns the value to cache
// - dependencies: Array of values that trigger recalculation
When React Re-calculates
function MemoBasicsDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState("John");
const [items, setItems] = useState([1, 2, 3, 4, 5]);
// 🎯 Without useMemo - runs on every render
const expensiveCalculationWithoutMemo = () => {
console.log("🔴 Expensive calculation running (no memo)");
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
};
// 🎯 With useMemo - only runs when dependencies change
const expensiveCalculationWithMemo = useMemo(() => {
console.log("🟢 Expensive calculation running (with memo)");
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
}, [count]); // Only recalculates when count changes
// 🎯 Array processing with memo
const processedItems = useMemo(() => {
console.log("🟡 Processing items array");
return items.map((item) => ({
id: item,
value: item * 2,
label: `Item ${item}`,
isEven: item % 2 === 0,
}));
}, [items]); // Only recalculates when items array changes
// 🎯 Complex object creation
const userProfile = useMemo(() => {
console.log("🔵 Creating user profile object");
return {
name,
displayName: name.toUpperCase(),
initials: name
.split(" ")
.map((n) => n[0])
.join(""),
metadata: {
createdAt: new Date().toISOString(),
count,
hasItems: items.length > 0,
},
};
}, [name, count, items.length]); // Recalculates when any dependency changes
const renderCount = useRef(0);
renderCount.current += 1;
return (
<div className="memo-basics-demo">
<h3>useMemo Basics Demo</h3>
<div className="render-info">
<p>
<strong>Render count:</strong> {renderCount.current}
</p>
<p>
<em>Check console to see when calculations run</em>
</p>
</div>
<div className="controls">
<div className="control-group">
<label>Count (affects memoized calculation):</label>
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
</div>
<div className="control-group">
<label>Name (affects user profile):</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
</div>
<div className="control-group">
<label>Items (affects processed items):</label>
<button
onClick={() => setItems((prev) => [...prev, prev.length + 1])}
>
Add Item
</button>
<button onClick={() => setItems((prev) => prev.slice(0, -1))}>
Remove Item
</button>
</div>
<div className="control-group">
<label>Force re-render (no state change):</label>
<button onClick={() => setCount(count)}>Force Re-render</button>
</div>
</div>
<div className="results">
<div className="result-section">
<h4>Without Memo (runs every render)</h4>
<p>Result: {expensiveCalculationWithoutMemo().toFixed(2)}</p>
</div>
<div className="result-section">
<h4>With Memo (runs only when count changes)</h4>
<p>Result: {expensiveCalculationWithMemo.toFixed(2)}</p>
</div>
<div className="result-section">
<h4>Processed Items</h4>
<div className="items-grid">
{processedItems.map((item) => (
<div
key={item.id}
className={`item ${item.isEven ? "even" : "odd"}`}
>
{item.label}: {item.value}
</div>
))}
</div>
</div>
<div className="result-section">
<h4>User Profile</h4>
<pre>{JSON.stringify(userProfile, null, 2)}</pre>
</div>
</div>
<div className="explanation">
<h4>What's Happening:</h4>
<ul>
<li>
<strong>Without memo:</strong> Calculation runs on every render
(expensive!)
</li>
<li>
<strong>With memo:</strong> Calculation only runs when dependencies
change
</li>
<li>
<strong>Processed items:</strong> Only recalculates when items array
changes
</li>
<li>
<strong>User profile:</strong> Only recalculates when name, count,
or items.length changes
</li>
</ul>
</div>
</div>
);
}
💰 Expensive Calculations
Identifying Expensive Operations
function ExpensiveCalculationsDemo() {
const [data, setData] = useState(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
value: Math.random() * 100,
category: ["A", "B", "C"][Math.floor(Math.random() * 3)],
}))
);
const [filterCategory, setFilterCategory] = useState("all");
const [sortBy, setSortBy] = useState("id");
const [searchTerm, setSearchTerm] = useState("");
const [threshold, setThreshold] = useState(50);
// 🎯 Expensive filtering and sorting
const processedData = useMemo(() => {
console.time("Data processing");
console.log("🔄 Processing data...");
let result = data;
// Filter by category
if (filterCategory !== "all") {
result = result.filter((item) => item.category === filterCategory);
}
// Filter by search term (expensive string operations)
if (searchTerm) {
result = result.filter(
(item) =>
item.id.toString().includes(searchTerm) ||
item.category.toLowerCase().includes(searchTerm.toLowerCase())
);
}
// Filter by threshold
result = result.filter((item) => item.value >= threshold);
// Sort (expensive for large arrays)
result = result.sort((a, b) => {
switch (sortBy) {
case "value":
return b.value - a.value;
case "category":
return a.category.localeCompare(b.category);
default:
return a.id - b.id;
}
});
console.timeEnd("Data processing");
return result;
}, [data, filterCategory, searchTerm, threshold, sortBy]);
// 🎯 Expensive statistical calculations
const statistics = useMemo(() => {
console.time("Statistics calculation");
console.log("📊 Calculating statistics...");
if (processedData.length === 0) {
return {
count: 0,
average: 0,
min: 0,
max: 0,
median: 0,
standardDeviation: 0,
};
}
const values = processedData.map((item) => item.value);
const sum = values.reduce((acc, val) => acc + val, 0);
const average = sum / values.length;
const sortedValues = [...values].sort((a, b) => a - b);
const median =
sortedValues.length % 2 === 0
? (sortedValues[sortedValues.length / 2 - 1] +
sortedValues[sortedValues.length / 2]) /
2
: sortedValues[Math.floor(sortedValues.length / 2)];
const variance =
values.reduce((acc, val) => acc + Math.pow(val - average, 2), 0) /
values.length;
const standardDeviation = Math.sqrt(variance);
console.timeEnd("Statistics calculation");
return {
count: processedData.length,
average: average.toFixed(2),
min: Math.min(...values).toFixed(2),
max: Math.max(...values).toFixed(2),
median: median.toFixed(2),
standardDeviation: standardDeviation.toFixed(2),
};
}, [processedData]);
// 🎯 Expensive chart data preparation
const chartData = useMemo(() => {
console.time("Chart data preparation");
console.log("📈 Preparing chart data...");
// Group by category and calculate averages
const categoryGroups = processedData.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item.value);
return acc;
}, {});
const chartData = Object.entries(categoryGroups).map(
([category, values]) => ({
category,
average: values.reduce((sum, val) => sum + val, 0) / values.length,
count: values.length,
min: Math.min(...values),
max: Math.max(...values),
})
);
console.timeEnd("Chart data preparation");
return chartData;
}, [processedData]);
const addRandomData = () => {
const newItems = Array.from({ length: 1000 }, (_, i) => ({
id: data.length + i,
value: Math.random() * 100,
category: ["A", "B", "C"][Math.floor(Math.random() * 3)],
}));
setData((prev) => [...prev, ...newItems]);
};
const resetData = () => {
setData(
Array.from({ length: 10000 }, (_, i) => ({
id: i,
value: Math.random() * 100,
category: ["A", "B", "C"][Math.floor(Math.random() * 3)],
}))
);
};
return (
<div className="expensive-calculations-demo">
<h3>Expensive Calculations Demo</h3>
<div className="performance-note">
<p>
<strong>📊 Performance Note:</strong> Open DevTools Console to see
timing information
</p>
</div>
<div className="controls">
<div className="control-row">
<div className="control-group">
<label>Filter Category:</label>
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value)}
>
<option value="all">All Categories</option>
<option value="A">Category A</option>
<option value="B">Category B</option>
<option value="C">Category C</option>
</select>
</div>
<div className="control-group">
<label>Sort By:</label>
<select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
<option value="id">ID</option>
<option value="value">Value (High to Low)</option>
<option value="category">Category</option>
</select>
</div>
</div>
<div className="control-row">
<div className="control-group">
<label>Search:</label>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search by ID or category"
/>
</div>
<div className="control-group">
<label>Minimum Value: {threshold}</label>
<input
type="range"
min="0"
max="100"
value={threshold}
onChange={(e) => setThreshold(Number(e.target.value))}
/>
</div>
</div>
<div className="control-row">
<button onClick={addRandomData}>Add 1000 Random Items</button>
<button onClick={resetData}>Reset Data</button>
</div>
</div>
<div className="results">
<div className="result-section">
<h4>Data Summary</h4>
<p>Total items: {data.length}</p>
<p>Filtered items: {processedData.length}</p>
<p>Processing time: Check console</p>
</div>
<div className="result-section">
<h4>Statistics</h4>
<div className="stats-grid">
<div className="stat">
<strong>Count:</strong> {statistics.count}
</div>
<div className="stat">
<strong>Average:</strong> {statistics.average}
</div>
<div className="stat">
<strong>Min:</strong> {statistics.min}
</div>
<div className="stat">
<strong>Max:</strong> {statistics.max}
</div>
<div className="stat">
<strong>Median:</strong> {statistics.median}
</div>
<div className="stat">
<strong>Std Dev:</strong> {statistics.standardDeviation}
</div>
</div>
</div>
<div className="result-section">
<h4>Chart Data by Category</h4>
<div className="chart-data">
{chartData.map((item) => (
<div key={item.category} className="chart-item">
<h5>Category {item.category}</h5>
<p>Count: {item.count}</p>
<p>Average: {item.average.toFixed(2)}</p>
<p>
Range: {item.min.toFixed(2)} - {item.max.toFixed(2)}
</p>
</div>
))}
</div>
</div>
<div className="result-section">
<h4>Sample Data (First 10 items)</h4>
<div className="data-preview">
{processedData.slice(0, 10).map((item) => (
<div key={item.id} className="data-item">
ID: {item.id}, Value: {item.value.toFixed(2)}, Category:{" "}
{item.category}
</div>
))}
{processedData.length > 10 && (
<p>... and {processedData.length - 10} more items</p>
)}
</div>
</div>
</div>
<div className="optimization-notes">
<h4>Optimization Techniques Used:</h4>
<ul>
<li>
<strong>Data Processing:</strong> Memoized filtering, searching, and
sorting
</li>
<li>
<strong>Statistics:</strong> Memoized complex mathematical
calculations
</li>
<li>
<strong>Chart Data:</strong> Memoized data transformation for
visualization
</li>
<li>
<strong>Dependency Arrays:</strong> Only recalculate when relevant
inputs change
</li>
</ul>
</div>
</div>
);
}
🔗 Reference Stability
Object and Array Reference Stability
function ReferenceStabilityDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState("John");
const [items, setItems] = useState(["apple", "banana", "cherry"]);
// 🎯 Without useMemo - new object/array on every render
const unstableConfig = {
theme: "dark",
count,
features: ["feature1", "feature2"],
};
const unstableArray = items.map((item) => item.toUpperCase());
// 🎯 With useMemo - stable references
const stableConfig = useMemo(
() => ({
theme: "dark",
count,
features: ["feature1", "feature2"],
}),
[count]
); // Only creates new object when count changes
const stableArray = useMemo(
() => items.map((item) => item.toUpperCase()),
[items]
); // Only creates new array when items change
// 🎯 Complex object with nested properties
const complexConfig = useMemo(
() => ({
user: {
name,
preferences: {
theme: "dark",
notifications: true,
layout: "grid",
},
},
app: {
version: "1.0.0",
features: {
search: true,
export: count > 5,
advanced: count > 10,
},
},
data: {
items: items.length,
processed: items.map((item) => ({
original: item,
processed: item.toUpperCase(),
length: item.length,
})),
},
}),
[name, count, items]
);
// 🎯 Filtered and sorted data
const processedItems = useMemo(() => {
return items
.filter((item) => item.length > 3)
.sort()
.map((item) => ({
id: item,
display: item.charAt(0).toUpperCase() + item.slice(1),
metadata: {
length: item.length,
vowels: (item.match(/[aeiou]/gi) || []).length,
},
}));
}, [items]);
// 🎯 Reference tracking
const previousRefsRef = useRef({});
const renderCountRef = useRef(0);
renderCountRef.current += 1;
const checkReferenceStability = (name, current) => {
const previous = previousRefsRef.current[name];
const isStable = previous === current;
previousRefsRef.current[name] = current;
return isStable;
};
const addItem = () => {
const newItem = `item${items.length + 1}`;
setItems((prev) => [...prev, newItem]);
};
const removeItem = () => {
setItems((prev) => prev.slice(0, -1));
};
const updateName = (newName) => {
setName(newName);
};
return (
<div className="reference-stability-demo">
<h3>Reference Stability Demo</h3>
<div className="render-info">
<p>
<strong>Render #{renderCountRef.current}</strong>
</p>
<p>
<em>Watch reference stability indicators below</em>
</p>
</div>
<div className="controls">
<div className="control-group">
<button onClick={() => setCount((c) => c + 1)}>Count: {count}</button>
<button onClick={() => setCount(count)}>
Force Re-render (same count)
</button>
</div>
<div className="control-group">
<input
value={name}
onChange={(e) => updateName(e.target.value)}
placeholder="Enter name"
/>
</div>
<div className="control-group">
<button onClick={addItem}>Add Item</button>
<button onClick={removeItem}>Remove Item</button>
<span>Items: {items.join(", ")}</span>
</div>
</div>
<div className="reference-tracking">
<h4>Reference Stability Tracking</h4>
<div className="stability-grid">
<div className="stability-item">
<h5>Unstable Config Object</h5>
<div
className={`stability-indicator ${
checkReferenceStability("unstableConfig", unstableConfig)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("unstableConfig", unstableConfig)
? "✅ Stable"
: "❌ New Reference"}
</div>
<pre>{JSON.stringify(unstableConfig, null, 2)}</pre>
</div>
<div className="stability-item">
<h5>Stable Config Object (useMemo)</h5>
<div
className={`stability-indicator ${
checkReferenceStability("stableConfig", stableConfig)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("stableConfig", stableConfig)
? "✅ Stable"
: "🔄 Updated"}
</div>
<pre>{JSON.stringify(stableConfig, null, 2)}</pre>
</div>
<div className="stability-item">
<h5>Unstable Array</h5>
<div
className={`stability-indicator ${
checkReferenceStability("unstableArray", unstableArray)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("unstableArray", unstableArray)
? "✅ Stable"
: "❌ New Reference"}
</div>
<div>Array: [{unstableArray.join(", ")}]</div>
</div>
<div className="stability-item">
<h5>Stable Array (useMemo)</h5>
<div
className={`stability-indicator ${
checkReferenceStability("stableArray", stableArray)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("stableArray", stableArray)
? "✅ Stable"
: "🔄 Updated"}
</div>
<div>Array: [{stableArray.join(", ")}]</div>
</div>
</div>
<div className="complex-object">
<h5>Complex Configuration Object</h5>
<div
className={`stability-indicator ${
checkReferenceStability("complexConfig", complexConfig)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("complexConfig", complexConfig)
? "✅ Stable"
: "🔄 Updated"}
</div>
<details>
<summary>View Complex Config</summary>
<pre>{JSON.stringify(complexConfig, null, 2)}</pre>
</details>
</div>
<div className="processed-items">
<h5>Processed Items</h5>
<div
className={`stability-indicator ${
checkReferenceStability("processedItems", processedItems)
? "stable"
: "unstable"
}`}
>
{checkReferenceStability("processedItems", processedItems)
? "✅ Stable"
: "🔄 Updated"}
</div>
<div className="items-display">
{processedItems.map((item) => (
<div key={item.id} className="processed-item">
<strong>{item.display}</strong>
<br />
<small>
Length: {item.metadata.length}, Vowels: {item.metadata.vowels}
</small>
</div>
))}
</div>
</div>
</div>
<div className="explanation">
<h4>Reference Stability Impact:</h4>
<ul>
<li>
<strong>❌ Unstable references:</strong> Create new objects/arrays
every render
</li>
<li>
<strong>✅ Stable references:</strong> Only create new when
dependencies change
</li>
<li>
<strong>🔄 Updated references:</strong> New reference due to
dependency change
</li>
<li>
<strong>Performance:</strong> Stable references prevent unnecessary
child re-renders
</li>
<li>
<strong>useEffect:</strong> Stable references prevent infinite loops
</li>
</ul>
</div>
</div>
);
}
Child Component Re-render Prevention
// 🎯 Child component that shows re-render behavior
const ExpensiveChild = React.memo(({ config, onAction }) => {
const renderCount = useRef(0);
renderCount.current += 1;
console.log(`ExpensiveChild rendered ${renderCount.current} times`);
// Simulate expensive rendering
const expensiveValue = useMemo(() => {
console.log("ExpensiveChild: Performing expensive calculation");
let result = 0;
for (let i = 0; i < 100000; i++) {
result += Math.random();
}
return result;
}, [config.seed]);
return (
<div className="expensive-child">
<h4>Expensive Child Component</h4>
<p>Render count: {renderCount.current}</p>
<p>Config theme: {config.theme}</p>
<p>Config seed: {config.seed}</p>
<p>Expensive value: {expensiveValue.toFixed(2)}</p>
<button onClick={() => onAction("child-action")}>Child Action</button>
</div>
);
});
function ChildReRenderDemo() {
const [parentCount, setParentCount] = useState(0);
const [childSeed, setChildSeed] = useState(1);
const [theme, setTheme] = useState("light");
// 🎯 Unstable config - causes unnecessary re-renders
const unstableConfig = {
theme,
seed: childSeed,
timestamp: Date.now(), // ❌ Always different!
};
// 🎯 Stable config - only changes when dependencies change
const stableConfig = useMemo(
() => ({
theme,
seed: childSeed,
// timestamp: Date.now() // ❌ Don't include changing values
}),
[theme, childSeed]
);
// 🎯 Unstable callback - new function every render
const unstableCallback = (action) => {
console.log("Action:", action, "Parent count:", parentCount);
};
// 🎯 Stable callback - memoized function
const stableCallback = useCallback(
(action) => {
console.log("Action:", action, "Parent count:", parentCount);
},
[parentCount]
);
const parentRenderCount = useRef(0);
parentRenderCount.current += 1;
return (
<div className="child-rerender-demo">
<h3>Child Re-render Prevention Demo</h3>
<div className="parent-info">
<p>
<strong>Parent render count:</strong> {parentRenderCount.current}
</p>
<p>
<em>Check console for child render logs</em>
</p>
</div>
<div className="controls">
<div className="control-group">
<button onClick={() => setParentCount((c) => c + 1)}>
Parent Count: {parentCount}
</button>
<p>
<em>This should NOT cause child re-renders with stable props</em>
</p>
</div>
<div className="control-group">
<button onClick={() => setChildSeed((s) => s + 1)}>
Child Seed: {childSeed}
</button>
<p>
<em>This SHOULD cause child re-renders</em>
</p>
</div>
<div className="control-group">
<button
onClick={() => setTheme((t) => (t === "light" ? "dark" : "light"))}
>
Theme: {theme}
</button>
<p>
<em>This SHOULD cause child re-renders</em>
</p>
</div>
</div>
<div className="children-comparison">
<div className="child-section">
<h4>❌ With Unstable Props</h4>
<p>
<em>Re-renders on every parent render</em>
</p>
<ExpensiveChild config={unstableConfig} onAction={unstableCallback} />
</div>
<div className="child-section">
<h4>✅ With Stable Props (useMemo + useCallback)</h4>
<p>
<em>Only re-renders when props actually change</em>
</p>
<ExpensiveChild config={stableConfig} onAction={stableCallback} />
</div>
</div>
<div className="explanation">
<h4>Optimization Techniques:</h4>
<ul>
<li>
<strong>React.memo:</strong> Prevents re-renders when props haven't
changed
</li>
<li>
<strong>useMemo:</strong> Stabilizes object/array references
</li>
<li>
<strong>useCallback:</strong> Stabilizes function references
</li>
<li>
<strong>Dependency arrays:</strong> Control when memoization updates
</li>
</ul>
</div>
</div>
);
}
🎯 Dependency Array Best Practices
Dependency Array Patterns
function DependencyArrayDemo() {
const [user, setUser] = useState({
name: "John",
age: 30,
email: "john@example.com",
});
const [settings, setSettings] = useState({
theme: "light",
notifications: true,
});
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const [filter, setFilter] = useState("");
// 🎯 Primitive dependencies
const userDisplayName = useMemo(() => {
console.log("Computing user display name");
return `${user.name} (${user.age} years old)`;
}, [user.name, user.age]); // ✅ Only specific properties
// ❌ WRONG: Entire object as dependency
const wrongUserDisplay = useMemo(() => {
console.log("Computing wrong user display (will run too often)");
return `${user.name} (${user.age} years old)`;
}, [user]); // ❌ Runs when ANY user property changes
// 🎯 Array length as dependency
const itemsInfo = useMemo(() => {
console.log("Computing items info");
return {
count: items.length,
isEmpty: items.length === 0,
hasMany: items.length > 10,
};
}, [items.length]); // ✅ Only when length changes
// 🎯 Computed dependencies
const hasNotifications = settings.notifications;
const isDarkTheme = settings.theme === "dark";
const uiConfig = useMemo(() => {
console.log("Computing UI config");
return {
showNotificationBadge: hasNotifications,
darkMode: isDarkTheme,
className: `theme-${settings.theme} ${
hasNotifications ? "with-notifications" : ""
}`,
styles: {
backgroundColor: isDarkTheme ? "#333" : "#fff",
color: isDarkTheme ? "#fff" : "#333",
},
};
}, [hasNotifications, isDarkTheme, settings.theme]); // ✅ Specific computed values
// 🎯 Complex filtering with multiple dependencies
const filteredItems = useMemo(() => {
console.log("Filtering items");
if (!filter) return items;
return items.filter((item) => {
const itemStr = item.toString();
return itemStr.includes(filter);
});
}, [items, filter]); // ✅ Both items and filter
// 🎯 Expensive calculation with conditional dependencies
const expensiveCalculation = useMemo(() => {
console.log("Performing expensive calculation");
// Only calculate if we have items and user is adult
if (items.length === 0 || user.age < 18) {
return { result: 0, message: "No calculation needed" };
}
let result = 0;
for (let i = 0; i < items.length * 1000; i++) {
result += Math.random() * user.age;
}
return {
result: result.toFixed(2),
message: `Calculated for ${items.length} items and age ${user.age}`,
};
}, [items.length, user.age]); // ✅ Only the values we actually use
// 🎯 Object with stable keys
const stableObjectKeys = useMemo(() => Object.keys(user).sort(), [user]);
const userMetadata = useMemo(() => {
console.log("Computing user metadata");
return {
fieldCount: stableObjectKeys.length,
hasEmail: stableObjectKeys.includes("email"),
hasAge: stableObjectKeys.includes("age"),
summary: `User has ${
stableObjectKeys.length
} fields: ${stableObjectKeys.join(", ")}`,
};
}, [stableObjectKeys]); // ✅ Depends on stable key array
// 🎯 Functions in dependencies (should be memoized)
const processItem = useCallback(
(item) => {
return {
original: item,
doubled: item * 2,
userAgeMultiplied: item * user.age,
};
},
[user.age]
);
const processedItems = useMemo(() => {
console.log("Processing items with function");
return filteredItems.map(processItem);
}, [filteredItems, processItem]); // ✅ processItem is memoized
const updateUser = (field, value) => {
setUser((prev) => ({ ...prev, [field]: value }));
};
const updateSettings = (field, value) => {
setSettings((prev) => ({ ...prev, [field]: value }));
};
const addItem = () => {
setItems((prev) => [...prev, Math.max(...prev) + 1]);
};
const removeItem = () => {
setItems((prev) => prev.slice(0, -1));
};
return (
<div className="dependency-array-demo">
<h3>Dependency Array Best Practices</h3>
<div className="controls">
<div className="control-section">
<h4>User Controls</h4>
<div className="control-group">
<label>Name:</label>
<input
value={user.name}
onChange={(e) => updateUser("name", e.target.value)}
/>
</div>
<div className="control-group">
<label>Age:</label>
<input
type="number"
value={user.age}
onChange={(e) => updateUser("age", Number(e.target.value))}
/>
</div>
<div className="control-group">
<label>Email:</label>
<input
value={user.email}
onChange={(e) => updateUser("email", e.target.value)}
/>
</div>
</div>
<div className="control-section">
<h4>Settings Controls</h4>
<div className="control-group">
<label>Theme:</label>
<select
value={settings.theme}
onChange={(e) => updateSettings("theme", e.target.value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div className="control-group">
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) =>
updateSettings("notifications", e.target.checked)
}
/>
Notifications
</label>
</div>
</div>
<div className="control-section">
<h4>Items Controls</h4>
<div className="control-group">
<button onClick={addItem}>Add Item</button>
<button onClick={removeItem}>Remove Item</button>
<span>Items: [{items.join(", ")}]</span>
</div>
<div className="control-group">
<label>Filter:</label>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items"
/>
</div>
</div>
</div>
<div className="results">
<div className="result-section">
<h4>User Display Names</h4>
<p>
<strong>Optimized (name + age only):</strong> {userDisplayName}
</p>
<p>
<strong>Unoptimized (entire user object):</strong>{" "}
{wrongUserDisplay}
</p>
<p>
<em>Check console - unoptimized version runs more often</em>
</p>
</div>
<div className="result-section">
<h4>Items Information</h4>
<pre>{JSON.stringify(itemsInfo, null, 2)}</pre>
</div>
<div className="result-section">
<h4>UI Configuration</h4>
<div style={uiConfig.styles} className={uiConfig.className}>
<p>Theme: {settings.theme}</p>
<p>
Notifications: {settings.notifications ? "Enabled" : "Disabled"}
</p>
<p>Dark mode: {uiConfig.darkMode ? "Yes" : "No"}</p>
</div>
</div>
<div className="result-section">
<h4>Filtered Items</h4>
<p>Filter: "{filter}"</p>
<p>Results: [{filteredItems.join(", ")}]</p>
</div>
<div className="result-section">
<h4>Expensive Calculation</h4>
<p>Result: {expensiveCalculation.result}</p>
<p>Message: {expensiveCalculation.message}</p>
</div>
<div className="result-section">
<h4>User Metadata</h4>
<pre>{JSON.stringify(userMetadata, null, 2)}</pre>
</div>
<div className="result-section">
<h4>Processed Items</h4>
<div className="processed-items">
{processedItems.map((item, index) => (
<div key={index} className="processed-item">
Original: {item.original}, Doubled: {item.doubled}, Age
Multiplied: {item.userAgeMultiplied}
</div>
))}
</div>
</div>
</div>
<div className="best-practices">
<h4>Dependency Array Best Practices:</h4>
<ul>
<li>
<strong>✅ Use specific properties:</strong> [user.name, user.age]
instead of [user]
</li>
<li>
<strong>✅ Use computed values:</strong> [items.length] instead of
[items]
</li>
<li>
<strong>✅ Memoize functions:</strong> Use useCallback for function
dependencies
</li>
<li>
<strong>✅ Stable references:</strong> Extract stable values outside
useMemo
</li>
<li>
<strong>❌ Avoid objects/arrays:</strong> Unless you need the entire
reference
</li>
<li>
<strong>❌ Don't omit dependencies:</strong> Include all values used
inside
</li>
</ul>
</div>
</div>
);
}
⚠️ Common Mistakes & Anti-Patterns
1. Premature Optimization
// ❌ WRONG: Memoizing everything unnecessarily
function OverMemoizedComponent() {
const [count, setCount] = useState(0);
// ❌ Unnecessary - simple calculation
const doubledCount = useMemo(() => count * 2, [count]);
// ❌ Unnecessary - primitive value
const isEven = useMemo(() => count % 2 === 0, [count]);
// ❌ Unnecessary - simple string
const message = useMemo(() => `Count is ${count}`, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<p>Is Even: {isEven ? "Yes" : "No"}</p>
<p>{message}</p>
</div>
);
}
// ✅ CORRECT: Only memoize when necessary
function OptimallyMemoizedComponent() {
const [count, setCount] = useState(0);
// ✅ Simple calculations - no memo needed
const doubledCount = count * 2;
const isEven = count % 2 === 0;
const message = `Count is ${count}`;
// ✅ Only memoize expensive operations
const expensiveCalculation = useMemo(() => {
let result = 0;
for (let i = 0; i < count * 100000; i++) {
result += Math.random();
}
return result;
}, [count]);
return (
<div>
<p>Count: {count}</p>
<p>Doubled: {doubledCount}</p>
<p>Is Even: {isEven ? "Yes" : "No"}</p>
<p>{message}</p>
<p>Expensive: {expensiveCalculation.toFixed(2)}</p>
</div>
);
}
2. Missing Dependencies
// ❌ WRONG: Missing dependencies
function MissingDependenciesExample() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
// ❌ Missing 'multiplier' in dependencies
const calculation = useMemo(() => {
return count * multiplier; // Uses multiplier but not in deps
}, [count]); // ❌ Stale closure!
return (
<div>
<p>Count: {count}</p>
<p>Multiplier: {multiplier}</p>
<p>Result: {calculation}</p>
</div>
);
}
// ✅ CORRECT: All dependencies included
function CorrectDependenciesExample() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
// ✅ All dependencies included
const calculation = useMemo(() => {
return count * multiplier;
}, [count, multiplier]); // ✅ Complete dependency array
return (
<div>
<p>Count: {count}</p>
<p>Multiplier: {multiplier}</p>
<p>Result: {calculation}</p>
</div>
);
}
3. Object/Array Dependencies
// ❌ WRONG: Using entire objects as dependencies
function WrongObjectDependencies() {
const [user, setUser] = useState({
name: "John",
age: 30,
email: "john@example.com",
});
// ❌ Will run whenever ANY user property changes
const userDisplayName = useMemo(() => {
return `${user.name} (${user.age})`; // Only uses name and age
}, [user]); // ❌ Depends on entire user object
return <p>{userDisplayName}</p>;
}
// ✅ CORRECT: Use specific properties
function CorrectObjectDependencies() {
const [user, setUser] = useState({
name: "John",
age: 30,
email: "john@example.com",
});
// ✅ Only runs when name or age changes
const userDisplayName = useMemo(() => {
return `${user.name} (${user.age})`;
}, [user.name, user.age]); // ✅ Only specific properties
return <p>{userDisplayName}</p>;
}
4. Expensive Dependency Calculations
// ❌ WRONG: Expensive calculations in dependency array
function ExpensiveDependencyCalculation() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
// ❌ Expensive operation in dependency array
const processedItems = useMemo(() => {
return items.map((item) => item * 2);
}, [items.map((item) => item.toString())]); // ❌ Creates new array every render!
return (
<div>
{processedItems.map((item) => (
<div key={item}>{item}</div>
))}
</div>
);
}
// ✅ CORRECT: Simple dependencies
function SimpleDependencyCalculation() {
const [items, setItems] = useState([1, 2, 3, 4, 5]);
// ✅ Simple dependency
const processedItems = useMemo(() => {
return items.map((item) => ({
original: item,
doubled: item * 2,
stringified: item.toString(),
}));
}, [items]); // ✅ Simple array reference
return (
<div>
{processedItems.map((item) => (
<div key={item.original}>
{item.original} → {item.doubled} ({item.stringified})
</div>
))}
</div>
);
}
🧪 Mini Challenges
Challenge 1: Data Dashboard Optimization
Build a data dashboard that:
- Processes large datasets efficiently
- Calculates multiple statistics
- Filters and sorts data
- Only recalculates when necessary
💡 Solution
function DataDashboard() {
const [rawData] = useState(
Array.from({ length: 50000 }, (_, i) => ({
id: i,
value: Math.random() * 1000,
category: ["A", "B", "C", "D"][Math.floor(Math.random() * 4)],
date: new Date(
2023,
Math.floor(Math.random() * 12),
Math.floor(Math.random() * 28) + 1
),
status: ["active", "inactive", "pending"][Math.floor(Math.random() * 3)],
}))
);
const [filters, setFilters] = useState({
category: "all",
status: "all",
minValue: 0,
maxValue: 1000,
dateRange: "all",
});
const [sortConfig, setSortConfig] = useState({
field: "id",
direction: "asc",
});
// 🎯 Filtered data
const filteredData = useMemo(() => {
console.time("Data filtering");
let result = rawData;
if (filters.category !== "all") {
result = result.filter((item) => item.category === filters.category);
}
if (filters.status !== "all") {
result = result.filter((item) => item.status === filters.status);
}
result = result.filter(
(item) => item.value >= filters.minValue && item.value <= filters.maxValue
);
if (filters.dateRange !== "all") {
const now = new Date();
const cutoff = new Date();
switch (filters.dateRange) {
case "last30":
cutoff.setDate(now.getDate() - 30);
break;
case "last90":
cutoff.setDate(now.getDate() - 90);
break;
case "thisYear":
cutoff.setFullYear(now.getFullYear(), 0, 1);
break;
}
result = result.filter((item) => item.date >= cutoff);
}
console.timeEnd("Data filtering");
return result;
}, [rawData, filters]);
// 🎯 Sorted data
const sortedData = useMemo(() => {
console.time("Data sorting");
const result = [...filteredData].sort((a, b) => {
const aVal = a[sortConfig.field];
const bVal = b[sortConfig.field];
let comparison = 0;
if (aVal > bVal) comparison = 1;
if (aVal < bVal) comparison = -1;
return sortConfig.direction === "desc" ? -comparison : comparison;
});
console.timeEnd("Data sorting");
return result;
}, [filteredData, sortConfig]);
// 🎯 Statistics
const statistics = useMemo(() => {
console.time("Statistics calculation");
if (filteredData.length === 0) {
return {
count: 0,
sum: 0,
average: 0,
min: 0,
max: 0,
median: 0,
categoryBreakdown: {},
statusBreakdown: {},
};
}
const values = filteredData.map((item) => item.value);
const sum = values.reduce((acc, val) => acc + val, 0);
const sortedValues = [...values].sort((a, b) => a - b);
const median =
sortedValues.length % 2 === 0
? (sortedValues[sortedValues.length / 2 - 1] +
sortedValues[sortedValues.length / 2]) /
2
: sortedValues[Math.floor(sortedValues.length / 2)];
const categoryBreakdown = filteredData.reduce((acc, item) => {
acc[item.category] = (acc[item.category] || 0) + 1;
return acc;
}, {});
const statusBreakdown = filteredData.reduce((acc, item) => {
acc[item.status] = (acc[item.status] || 0) + 1;
return acc;
}, {});
console.timeEnd("Statistics calculation");
return {
count: filteredData.length,
sum: sum.toFixed(2),
average: (sum / filteredData.length).toFixed(2),
min: Math.min(...values).toFixed(2),
max: Math.max(...values).toFixed(2),
median: median.toFixed(2),
categoryBreakdown,
statusBreakdown,
};
}, [filteredData]);
// 🎯 Chart data
const chartData = useMemo(() => {
console.time("Chart data preparation");
const categoryData = Object.entries(statistics.categoryBreakdown).map(
([category, count]) => ({
category,
count,
percentage: ((count / statistics.count) * 100).toFixed(1),
})
);
const statusData = Object.entries(statistics.statusBreakdown).map(
([status, count]) => ({
status,
count,
percentage: ((count / statistics.count) * 100).toFixed(1),
})
);
console.timeEnd("Chart data preparation");
return { categoryData, statusData };
}, [statistics]);
const updateFilter = (key, value) => {
setFilters((prev) => ({ ...prev, [key]: value }));
};
const updateSort = (field) => {
setSortConfig((prev) => ({
field,
direction:
prev.field === field && prev.direction === "asc" ? "desc" : "asc",
}));
};
return (
<div className="data-dashboard">
<h3>Optimized Data Dashboard</h3>
{/* Filters */}
<div className="filters">
<div className="filter-group">
<label>Category:</label>
<select
value={filters.category}
onChange={(e) => updateFilter("category", e.target.value)}
>
<option value="all">All</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
<option value="D">D</option>
</select>
</div>
<div className="filter-group">
<label>Status:</label>
<select
value={filters.status}
onChange={(e) => updateFilter("status", e.target.value)}
>
<option value="all">All</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
<option value="pending">Pending</option>
</select>
</div>
<div className="filter-group">
<label>Value Range:</label>
<input
type="range"
min="0"
max="1000"
value={filters.minValue}
onChange={(e) => updateFilter("minValue", Number(e.target.value))}
/>
<span>
{filters.minValue} - {filters.maxValue}
</span>
<input
type="range"
min="0"
max="1000"
value={filters.maxValue}
onChange={(e) => updateFilter("maxValue", Number(e.target.value))}
/>
</div>
</div>
{/* Statistics */}
<div className="statistics">
<h4>Statistics</h4>
<div className="stats-grid">
<div className="stat">Count: {statistics.count}</div>
<div className="stat">Sum: {statistics.sum}</div>
<div className="stat">Average: {statistics.average}</div>
<div className="stat">Min: {statistics.min}</div>
<div className="stat">Max: {statistics.max}</div>
<div className="stat">Median: {statistics.median}</div>
</div>
</div>
{/* Charts */}
<div className="charts">
<div className="chart">
<h5>Category Breakdown</h5>
{chartData.categoryData.map((item) => (
<div key={item.category} className="chart-bar">
{item.category}: {item.count} ({item.percentage}%)
</div>
))}
</div>
<div className="chart">
<h5>Status Breakdown</h5>
{chartData.statusData.map((item) => (
<div key={item.status} className="chart-bar">
{item.status}: {item.count} ({item.percentage}%)
</div>
))}
</div>
</div>
{/* Data Table */}
<div className="data-table">
<h4>Data (First 100 rows)</h4>
<table>
<thead>
<tr>
<th onClick={() => updateSort("id")}>ID</th>
<th onClick={() => updateSort("value")}>Value</th>
<th onClick={() => updateSort("category")}>Category</th>
<th onClick={() => updateSort("status")}>Status</th>
<th onClick={() => updateSort("date")}>Date</th>
</tr>
</thead>
<tbody>
{sortedData.slice(0, 100).map((item) => (
<tr key={item.id}>
<td>{item.id}</td>
<td>{item.value.toFixed(2)}</td>
<td>{item.category}</td>
<td>{item.status}</td>
<td>{item.date.toLocaleDateString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Challenge 2: Smart Shopping Cart
Create a shopping cart that:
- Calculates totals, taxes, and discounts efficiently
- Handles complex pricing rules
- Updates only when necessary
- Provides real-time validation
💡 Solution
function SmartShoppingCart() {
const [items, setItems] = useState([
{
id: 1,
name: "Laptop",
price: 999.99,
quantity: 1,
category: "electronics",
taxable: true,
},
{
id: 2,
name: "Book",
price: 29.99,
quantity: 2,
category: "books",
taxable: false,
},
{
id: 3,
name: "Headphones",
price: 199.99,
quantity: 1,
category: "electronics",
taxable: true,
},
]);
const [discountCode, setDiscountCode] = useState("");
const [shippingMethod, setShippingMethod] = useState("standard");
const [customerType, setCustomerType] = useState("regular");
// 🎯 Discount rules
const discountRules = useMemo(
() => ({
SAVE10: { type: "percentage", value: 0.1, minAmount: 50 },
ELECTRONICS20: {
type: "percentage",
value: 0.2,
category: "electronics",
},
FREESHIP: { type: "shipping", value: 0 },
STUDENT15: { type: "percentage", value: 0.15, customerType: "student" },
}),
[]
);
// 🎯 Shipping rates
const shippingRates = useMemo(
() => ({
standard: 9.99,
express: 19.99,
overnight: 39.99,
free: 0,
}),
[]
);
// 🎯 Tax rates by category
const taxRates = useMemo(
() => ({
electronics: 0.08,
books: 0,
clothing: 0.06,
food: 0.03,
}),
[]
);
// 🎯 Calculate subtotal
const subtotal = useMemo(() => {
console.log("Calculating subtotal");
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}, [items]);
// 🎯 Calculate item-level discounts
const itemDiscounts = useMemo(() => {
console.log("Calculating item discounts");
const discount = discountRules[discountCode];
if (!discount) return {};
const discounts = {};
items.forEach((item) => {
let itemDiscount = 0;
if (discount.type === "percentage") {
// Category-specific discount
if (discount.category && item.category === discount.category) {
itemDiscount = item.price * item.quantity * discount.value;
}
// Customer type discount
else if (
discount.customerType &&
customerType === discount.customerType
) {
itemDiscount = item.price * item.quantity * discount.value;
}
// General percentage discount
else if (!discount.category && !discount.customerType) {
itemDiscount = item.price * item.quantity * discount.value;
}
}
if (itemDiscount > 0) {
discounts[item.id] = itemDiscount;
}
});
return discounts;
}, [items, discountCode, customerType, discountRules]);
// 🎯 Calculate total discount
const totalDiscount = useMemo(() => {
console.log("Calculating total discount");
const itemDiscountTotal = Object.values(itemDiscounts).reduce(
(sum, discount) => sum + discount,
0
);
const discount = discountRules[discountCode];
// Check minimum amount requirement
if (discount && discount.minAmount && subtotal < discount.minAmount) {
return 0;
}
return itemDiscountTotal;
}, [itemDiscounts, discountRules, discountCode, subtotal]);
// 🎯 Calculate taxes
const taxes = useMemo(() => {
console.log("Calculating taxes");
const discountedSubtotal = subtotal - totalDiscount;
return items.reduce((totalTax, item) => {
if (!item.taxable) return totalTax;
const taxRate = taxRates[item.category] || 0;
const itemTotal = item.price * item.quantity;
const itemDiscountRatio = (itemDiscounts[item.id] || 0) / itemTotal;
const discountedItemTotal = itemTotal * (1 - itemDiscountRatio);
return totalTax + discountedItemTotal * taxRate;
}, 0);
}, [items, subtotal, totalDiscount, itemDiscounts, taxRates]);
// 🎯 Calculate shipping
const shipping = useMemo(() => {
console.log("Calculating shipping");
const discount = discountRules[discountCode];
if (discount && discount.type === "shipping") {
return discount.value;
}
// Free shipping for orders over $100
if (subtotal - totalDiscount > 100) {
return 0;
}
return shippingRates[shippingMethod] || 0;
}, [
discountCode,
discountRules,
subtotal,
totalDiscount,
shippingMethod,
shippingRates,
]);
// 🎯 Calculate final total
const total = useMemo(() => {
console.log("Calculating final total");
return subtotal - totalDiscount + taxes + shipping;
}, [subtotal, totalDiscount, taxes, shipping]);
// 🎯 Validation messages
const validationMessages = useMemo(() => {
console.log("Calculating validation messages");
const messages = [];
const discount = discountRules[discountCode];
if (discountCode && !discount) {
messages.push({ type: "error", text: "Invalid discount code" });
}
if (discount && discount.minAmount && subtotal < discount.minAmount) {
messages.push({
type: "warning",
text: `Add $${(discount.minAmount - subtotal).toFixed(
2
)} more to qualify for discount`,
});
}
if (
discount &&
discount.customerType &&
customerType !== discount.customerType
) {
messages.push({
type: "error",
text: `This discount is only for ${discount.customerType} customers`,
});
}
if (subtotal - totalDiscount > 100 && shipping > 0) {
messages.push({
type: "info",
text: "You qualify for free shipping!",
});
}
return messages;
}, [
discountCode,
discountRules,
subtotal,
totalDiscount,
customerType,
shipping,
]);
const updateQuantity = (id, quantity) => {
setItems((prev) =>
prev.map((item) =>
item.id === id ? { ...item, quantity: Math.max(0, quantity) } : item
)
);
};
const removeItem = (id) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
return (
<div className="smart-shopping-cart">
<h3>Smart Shopping Cart</h3>
{/* Controls */}
<div className="controls">
<div className="control-group">
<label>Customer Type:</label>
<select
value={customerType}
onChange={(e) => setCustomerType(e.target.value)}
>
<option value="regular">Regular</option>
<option value="student">Student</option>
<option value="premium">Premium</option>
</select>
</div>
<div className="control-group">
<label>Discount Code:</label>
<input
value={discountCode}
onChange={(e) => setDiscountCode(e.target.value.toUpperCase())}
placeholder="Enter discount code"
/>
</div>
<div className="control-group">
<label>Shipping:</label>
<select
value={shippingMethod}
onChange={(e) => setShippingMethod(e.target.value)}
>
<option value="standard">Standard ($9.99)</option>
<option value="express">Express ($19.99)</option>
<option value="overnight">Overnight ($39.99)</option>
</select>
</div>
</div>
{/* Validation Messages */}
{validationMessages.length > 0 && (
<div className="validation-messages">
{validationMessages.map((message, index) => (
<div key={index} className={`message ${message.type}`}>
{message.text}
</div>
))}
</div>
)}
{/* Cart Items */}
<div className="cart-items">
<h4>Cart Items</h4>
{items.map((item) => (
<div key={item.id} className="cart-item">
<div className="item-info">
<h5>{item.name}</h5>
<p>Category: {item.category}</p>
<p>Price: ${item.price}</p>
<p>Taxable: {item.taxable ? "Yes" : "No"}</p>
</div>
<div className="item-controls">
<button
onClick={() => updateQuantity(item.id, item.quantity - 1)}
>
-
</button>
<span>Qty: {item.quantity}</span>
<button
onClick={() => updateQuantity(item.id, item.quantity + 1)}
>
+
</button>
<button onClick={() => removeItem(item.id)}>Remove</button>
</div>
<div className="item-totals">
<p>Subtotal: ${(item.price * item.quantity).toFixed(2)}</p>
{itemDiscounts[item.id] && (
<p>Discount: -${itemDiscounts[item.id].toFixed(2)}</p>
)}
</div>
</div>
))}
</div>
{/* Cart Summary */}
<div className="cart-summary">
<h4>Order Summary</h4>
<div className="summary-line">
<span>Subtotal:</span>
<span>${subtotal.toFixed(2)}</span>
</div>
{totalDiscount > 0 && (
<div className="summary-line discount">
<span>Discount ({discountCode}):</span>
<span>-${totalDiscount.toFixed(2)}</span>
</div>
)}
<div className="summary-line">
<span>Taxes:</span>
<span>${taxes.toFixed(2)}</span>
</div>
<div className="summary-line">
<span>Shipping:</span>
<span>${shipping.toFixed(2)}</span>
</div>
<div className="summary-line total">
<span>
<strong>Total:</strong>
</span>
<span>
<strong>${total.toFixed(2)}</strong>
</span>
</div>
</div>
</div>
);
}
📊 Performance Measurement
Measuring useMemo Impact
function PerformanceMeasurement() {
const [dataSize, setDataSize] = useState(10000);
const [useOptimization, setUseOptimization] = useState(true);
const [triggerUpdate, setTriggerUpdate] = useState(0);
// Generate test data
const testData = useMemo(() => {
console.log("Generating test data");
return Array.from({ length: dataSize }, (_, i) => ({
id: i,
value: Math.random() * 100,
category: ["A", "B", "C"][Math.floor(Math.random() * 3)],
}));
}, [dataSize]);
// 🎯 Performance timing hook
const usePerformanceTimer = (label) => {
const startTimeRef = useRef(null);
const [duration, setDuration] = useState(0);
const start = useCallback(() => {
startTimeRef.current = performance.now();
}, []);
const end = useCallback(() => {
if (startTimeRef.current) {
const duration = performance.now() - startTimeRef.current;
setDuration(duration);
console.log(`${label}: ${duration.toFixed(2)}ms`);
}
}, [label]);
return { start, end, duration };
};
const optimizedTimer = usePerformanceTimer("Optimized calculation");
const unoptimizedTimer = usePerformanceTimer("Unoptimized calculation");
// 🎯 Expensive calculation with optimization
const optimizedResult = useMemo(() => {
if (!useOptimization) return null;
optimizedTimer.start();
const result = testData.reduce(
(acc, item) => {
acc.total += item.value;
acc.count += 1;
acc.categories[item.category] =
(acc.categories[item.category] || 0) + 1;
if (item.value > acc.max) acc.max = item.value;
if (item.value < acc.min) acc.min = item.value;
return acc;
},
{
total: 0,
count: 0,
categories: {},
max: -Infinity,
min: Infinity,
}
);
result.average = result.total / result.count;
optimizedTimer.end();
return result;
}, [testData, useOptimization, optimizedTimer]);
// 🎯 Expensive calculation without optimization
const calculateUnoptimized = () => {
if (useOptimization) return null;
unoptimizedTimer.start();
const result = testData.reduce(
(acc, item) => {
acc.total += item.value;
acc.count += 1;
acc.categories[item.category] =
(acc.categories[item.category] || 0) + 1;
if (item.value > acc.max) acc.max = item.value;
if (item.value < acc.min) acc.min = item.value;
return acc;
},
{
total: 0,
count: 0,
categories: {},
max: -Infinity,
min: Infinity,
}
);
result.average = result.total / result.count;
unoptimizedTimer.end();
return result;
};
const unoptimizedResult = !useOptimization ? calculateUnoptimized() : null;
const currentResult = useOptimization ? optimizedResult : unoptimizedResult;
const renderCount = useRef(0);
renderCount.current += 1;
return (
<div className="performance-measurement">
<h3>Performance Measurement Demo</h3>
<div className="controls">
<div className="control-group">
<label>Data Size:</label>
<select
value={dataSize}
onChange={(e) => setDataSize(Number(e.target.value))}
>
<option value={1000}>1,000 items</option>
<option value={10000}>10,000 items</option>
<option value={50000}>50,000 items</option>
<option value={100000}>100,000 items</option>
</select>
</div>
<div className="control-group">
<label>
<input
type="checkbox"
checked={useOptimization}
onChange={(e) => setUseOptimization(e.target.checked)}
/>
Use useMemo Optimization
</label>
</div>
<div className="control-group">
<button onClick={() => setTriggerUpdate((prev) => prev + 1)}>
Force Re-render (Trigger: {triggerUpdate})
</button>
</div>
</div>
<div className="performance-stats">
<h4>Performance Statistics</h4>
<div className="stats-grid">
<div className="stat">
<strong>Render Count:</strong> {renderCount.current}
</div>
<div className="stat">
<strong>Data Size:</strong> {dataSize.toLocaleString()} items
</div>
<div className="stat">
<strong>Optimization:</strong>{" "}
{useOptimization ? "Enabled" : "Disabled"}
</div>
<div className="stat">
<strong>Last Calculation Time:</strong>
{useOptimization
? `${optimizedTimer.duration.toFixed(2)}ms`
: `${unoptimizedTimer.duration.toFixed(2)}ms`}
</div>
</div>
</div>
{currentResult && (
<div className="calculation-results">
<h4>Calculation Results</h4>
<div className="results-grid">
<div className="result">
<strong>Total:</strong> {currentResult.total.toFixed(2)}
</div>
<div className="result">
<strong>Count:</strong> {currentResult.count}
</div>
<div className="result">
<strong>Average:</strong> {currentResult.average.toFixed(2)}
</div>
<div className="result">
<strong>Min:</strong> {currentResult.min.toFixed(2)}
</div>
<div className="result">
<strong>Max:</strong> {currentResult.max.toFixed(2)}
</div>
</div>
<div className="category-breakdown">
<h5>Category Breakdown:</h5>
{Object.entries(currentResult.categories).map(
([category, count]) => (
<div key={category} className="category-stat">
{category}: {count} items
</div>
)
)}
</div>
</div>
)}
<div className="performance-notes">
<h4>Performance Notes:</h4>
<ul>
<li>
<strong>With useMemo:</strong> Calculation only runs when data
changes
</li>
<li>
<strong>Without useMemo:</strong> Calculation runs on every render
</li>
<li>
<strong>Force re-render:</strong> Test how optimization affects
performance
</li>
<li>
<strong>Data size:</strong> Larger datasets show bigger performance
differences
</li>
</ul>
</div>
</div>
);
}
🎯 When and Why: useMemo Decision Framework
Quick Decision Tree
🤔 Should I use useMemo?
├── Is this an expensive calculation?
│ ├── Yes → Continue evaluation...
│ └── No → Don't use useMemo ❌
│
├── Does the calculation run on every render?
│ ├── Yes → Continue evaluation...
│ └── No → Don't use useMemo ❌
│
├── Do the inputs change frequently?
│ ├── Yes → useMemo might not help ⚠️
│ └── No → Continue evaluation...
│
├── Is this for reference stability?
│ ├── Yes → Use useMemo ✅
│ └── No → Continue evaluation...
│
├── Is this preventing child re-renders?
│ ├── Yes → Use useMemo ✅
│ └── No → Continue evaluation...
│
└── Is the calculation actually expensive?
├── Yes → Use useMemo ✅
└── No → Don't use useMemo ❌
Performance Guidelines
// 🎯 When TO use useMemo:
// ✅ Expensive calculations
const expensiveValue = useMemo(() => {
return heavyCalculation(data);
}, [data]);
// ✅ Object/array reference stability
const stableConfig = useMemo(
() => ({
theme: "dark",
features: ["a", "b", "c"],
}),
[]
);
// ✅ Preventing child re-renders
const memoizedProps = useMemo(
() => ({
data: processedData,
config: settings,
}),
[processedData, settings]
);
// ✅ Complex filtering/sorting
const filteredData = useMemo(() => {
return data
.filter((item) => item.active)
.sort((a, b) => a.name.localeCompare(b.name));
}, [data]);
// 🎯 When NOT to use useMemo:
// ❌ Simple calculations
const doubled = count * 2; // Don't memo
// ❌ Primitive values
const isEven = count % 2 === 0; // Don't memo
// ❌ Always changing dependencies
const timestamp = useMemo(() => Date.now(), [Date.now()]); // Useless
// ❌ More expensive than the calculation
const simple = useMemo(() => a + b, [a, b]); // Overhead > benefit
🎤 Interview Insights
Common Interview Questions
-
"When would you use useMemo?"
- Expensive calculations that don't need to run on every render
- Object/array reference stability for child components
- Complex data transformations (filtering, sorting, grouping)
- Preventing unnecessary re-renders in React.memo components
-
"What's the difference between useMemo and useCallback?"
- useMemo: Memoizes the result of a calculation
- useCallback: Memoizes the function itself
- Both prevent unnecessary re-computations
- Show examples of when to use each
-
"How do you measure if useMemo is actually helping?"
- Use React DevTools Profiler
- Console.time() for manual timing
- Performance.now() for precise measurements
- Compare render times with/without memoization
-
"What are the downsides of useMemo?"
- Memory overhead (storing cached values)
- Comparison overhead (checking dependencies)
- Can make code more complex
- Premature optimization can hurt performance
Code Review Red Flags
// 🚨 Red Flags in Interviews:
// ❌ Memoizing everything
const simple = useMemo(() => a + b, [a, b]);
// ❌ Missing dependencies
const calc = useMemo(() => a * b * c, [a, b]); // Missing 'c'
// ❌ Expensive dependencies
const result = useMemo(() => process(data), [data.map((x) => x.id)]);
// ❌ Object dependencies
const result = useMemo(() => process(user.name), [user]); // Use user.name
// ❌ Always changing dependencies
const result = useMemo(() => calculate(), [Math.random()]);
🎯 Key Takeaways
Mental Model
// 🧠 Think of useMemo as:
// "A cache that stores the result of expensive calculations
// and only recalculates when dependencies change"
const memoizedValue = useMemo(() => {
// This only runs when dependencies change
return expensiveCalculation(dependencies);
}, [dependencies]);
Best Practices Summary
- Profile first - Measure before optimizing
- Use for expensive operations - Not simple calculations
- Stabilize references - For objects/arrays passed to children
- Specific dependencies - Use primitive values when possible
- Don't over-memoize - It has overhead too
- Consider alternatives - Sometimes restructuring is better
- Test performance impact - Verify it actually helps
Production Tips
- Start without useMemo - Add only when needed
- Use React DevTools Profiler - Identify actual bottlenecks
- Consider component splitting - Sometimes better than memoization
- Watch bundle size - Don't memoize everything
- Document expensive operations - Help future developers understand
Next up: useCallback: Function Memoization - Master function memoization and prevent unnecessary re-renders.
Previous: useRef: Refs vs Variables
💡 Pro tip: In interviews, always explain the trade-offs. Show that you understand useMemo has overhead and should only be used when the benefits outweigh the costs. Demonstrate performance measurement techniques.