React Cheatsheets
KDP Book Manifest & MetadataClick to Expand & CopyClick to CollapseManifest
Use the copy buttons below to copy metadata verbatim into the Amazon KDP Publishing forms.
- Books > Computers & Technology > Programming > React
- Books > Computers & Technology > Web Development
React Cheatsheets
One-Page Quick References from Core Syntax to Advanced Patterns
React Cheatsheets
First Edition: 2026
Copyright © 2026 by usefulcheatsheets.com. All rights reserved.
No part of this book may be reproduced in any form or by any electronic or mechanical means, including information storage and retrieval systems, without written permission from the publisher, except for the use of brief quotations in a book review.
Table of Contents
Show or hide elements in React using if statements, ternaries, logical operators, and switch patterns.
Handle user events in React with camelCase handlers, synthetic events, and controlled inputs.
Build and manage controlled forms in React with inputs, validation, and submit handlers.
Master React.Fragment, short syntax, and when keys are necessary.
Render lists efficiently with the key prop, understand reconciliation, and avoid common list rendering bugs.
Master prop drilling, children, and patterns for composable component APIs.
Learn useRef fundamentals: DOM access, callback refs, useImperativeHandle, and React 19 ref-as-prop.
Manage React component state with useState to store and update values across renders.
Build accessible React apps using semantic HTML, ARIA attributes, and keyboard navigation.
Master React patterns like compound components, render props, and higher-order components for clean reusable patterns.
Table of Contents
Build reusable custom hooks to extract component logic and share stateful behavior.
Catch React errors with error boundaries, handle lifecycle errors, and display fallback UI gracefully.
Quick reference for core and advanced React hooks with usage rules and patterns.
Understand React component lifecycle phases and map class methods to hooks.
Use portals to render components outside the DOM hierarchy for modals and overlays.
Real-world React ref patterns: click-outside detection, ref arrays, previous value tracking, ResizeObserver, and TypeScript typing.
Manage application state with useState, useReducer, Context, Redux, or Zustand.
Load components and data with Suspense boundaries for better UX and streaming.
Test React components with Jest and React Testing Library to verify user behavior.
Test React components effectively using React Testing Library, Vitest, and common testing patterns.
Table of Contents
Advanced patterns for Context API, custom hooks, and state management.
Share state across React components without prop drilling using the useContext hook.
Handle side effects, manage dependencies, and clean up after effects.
Optimize React performance by memoizing values and callbacks to prevent unnecessary re-renders.
Manage complex state logic with the useReducer hook, actions, reducers, and common patterns.
Build scalable component architectures with established design patterns.
Optimize context usage to avoid unnecessary re-renders with splitting and memoization.
Optimize component rendering with React.memo, lazy loading, and code splitting strategies.
Welcome to React
React is a key topic in Technology development.
This reference book compiles comprehensive cheatsheets covering everything from fundamentals to advanced patterns.
Use this book as a daily reference or read it linearly to build your knowledge.
How to Use This Book
Each page is a visual cheatsheet with core concepts, practical steps, code snippets, and warnings.
React Conditional Rendering
Show or hide elements in React using if statements, ternaries, logical operators, and switch patterns.
TL;DR
- 01Use if statements to return different JSX based on conditions.
- 02Use ternary expressions inside JSX for inline conditionals.
- 03Use logical AND for simple show-or-hide patterns.
Tips
- 01Prefer if statements and switch for clarity, since they separate logic from JSX and are easier to test and refactor.
Warnings
- 01Avoid using <code>&&</code> with falsy values like <code>0</code> or empty strings, since they will render nothing instead of the string itself.
React Conditional Rendering
(continued)If Statements
- Return different JSX based on a condition before the return statement.
function UserInfo({ user }) { if (!user) return <p>Loading...</p>; return <div>{user.name}</div>; } - This is the clearest pattern when conditions are complex.
- Use early returns to handle edge cases at the top of the component.
- Combine multiple if statements for sequential checks.
- Avoid nesting too many ifs — extract sub-components instead.
React Conditional Rendering
(continued)Ternary Operator
- Use the ternary operator inside JSX for inline conditionals.
<div>{user ? user.name : "Guest"}</div> - Read as "if user is truthy, show user.name, else show Guest".
- Keep ternaries simple and readable — use if statements for complex logic.
- Nest ternaries only for quick reads, not for multiple conditions.
<div>{user ? (user.isAdmin ? "Admin" : "User") : "Guest"}</div> - Extract nested ternaries into separate components for clarity.
React Conditional Rendering
(continued)Logical AND Operator
- Use
&&to render JSX only when a condition is true.<div>{user && <p>Hello, {user.name}!</p>}</div> - This hides the element when the condition is false without rendering anything.
- Only use when there is no "else" case needed — otherwise use ternary.
- Beware of falsy values — avoid using
&&with0or empty strings.{count > 0 && <p>{count} items</p>} - Use ternary instead if you need to show something when the condition is false.
React Conditional Rendering
(continued)Switch Patterns
- Use a switch statement before the return for multiple conditions.
function Status({ status }) { switch (status) { case "loading": return <p>Loading...</p>; case "success": return <p>Done!</p>; case "error": return <p>Error occurred</p>; default: return null; } } - Switch works well when you have many distinct states to handle.
- Always include a default case for unexpected values.
- Extract each case into a separate component for reusability.
React Conditional Rendering
(continued)Conditional Components
- Create helper components to encapsulate conditional rendering logic.
function IfAdmin({ children, user }) { return user?.isAdmin ? children : null; } <IfAdmin user={user}> <AdminPanel /> </IfAdmin> - Use render props or children to control what is displayed.
- Name components clearly to show their conditional purpose.
- This pattern keeps components focused and testable.
- Combine with custom hooks for more complex conditional logic.
React Conditional Rendering
(FAQ)FAQ
Use a ternary inside JSX when you need to choose between two inline values or elements. Reach for an if statement when the logic is complex or you're returning entirely different component trees, since it keeps your JSX cleaner and the logic easier to test.
The && operator short-circuits to the left operand when it's falsy, and React renders the number 0. Convert the left side to a boolean first: use !!count &&
Use a switch statement or extract the logic into a helper function that returns JSX. This keeps your render method readable and makes each branch independently testable, especially when you have more than two possible states.
No, if statements are not expressions and can't appear inside JSX curly braces. Move the conditional logic above the return statement or wrap it in an immediately invoked function expression, though the former is almost always cleaner.
Use a ternary expression in the className prop: className={isActive ? 'active' : 'inactive'}. For multiple conditional classes, the classnames or clsx library keeps the logic readable without manual string concatenation.
React Event Handling
Handle user events in React with camelCase handlers, synthetic events, and controlled inputs.
TL;DR
- 01Use camelCase event names like onClick and onChange in JSX.
- 02Pass a function reference as the handler, not a function call.
- 03Use e.preventDefault() and e.stopPropagation() to control event flow.
Tips
- 01Always use controlled components for forms — it gives you better control over input values and validation.
Warnings
- 01Remember to call preventDefault() on forms to prevent page reload on submission.
React Event Handling
(continued)Basic Event Handlers
- Attach events with camelCase attribute names like
onClick, not lowercaseonclick.function Button() { function handleClick() { console.log("Button clicked"); } return <button onClick={handleClick}>Click me</button>; } - Pass a function reference, not a function call, or it runs on every render.
// Good: pass the function <button onClick={handleClick}>Click</button> // Bad: calls function immediately on render <button onClick={handleClick()}>Click</button> - Write inline handlers as arrow functions for short, one-off logic.
<button onClick={() => console.log("clicked")}>Click</button> - Pass extra arguments to a handler by wrapping the call in an arrow function.
function List({ items }) { return ( <ul> {items.map(item => ( <li key={item.id}> <button onClick={() => handleDelete(item.id)}> Delete {item.name} </button> </li> ))} </ul> ); } - Name handler functions with a
handleprefix so their purpose is obvious at a glance.function handleDelete(id) { /* ... */ } function handleToggle() { /* ... */ }
React Event Handling
(continued)Common Event Handlers
- Handle form submission with
onSubmiton the<form>element, notonClickon the button.function LoginForm() { const handleSubmit = (e) => { e.preventDefault(); console.log("Form submitted"); }; return <form onSubmit={handleSubmit}></form>; } - Handle input changes with
onChange, reading the new value frome.target.value.function NameInput() { const [name, setName] = useState(""); return <input value={name} onChange={(e) => setName(e.target.value)} />; } - Handle mouse events like
onMouseEnter,onMouseLeave, andonMouseMovefor hover effects.<div onMouseEnter={() => setHovered(true)}> Hover me </div> - Handle keyboard shortcuts with
onKeyDown, checkinge.keyfor the pressed key.function SearchBox() { const handleKeyDown = (e) => { if (e.key === "Enter") submitSearch(); if (e.key === "Escape") clearSearch(); }; return <input onKeyDown={handleKeyDown} placeholder="Search..." />; } - Handle focus and blur events with
onFocusandonBlurfor accessibility cues.function Input() { const [focused, setFocused] = useState(false); return ( <input className={focused ? "focused" : ""} onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} /> ); }
React Event Handling
(continued)Event Object
- Access event properties like target and preventDefault.
function handleClick(event) { console.log(event.target); // the element clicked console.log(event.type); // "click" } function handleSubmit(event) { event.preventDefault(); // stop form submission event.stopPropagation(); // stop event bubbling } - React's SyntheticEvent wraps the browser's native event for cross-browser consistency; access the underlying event with
e.nativeEvent.function handleClick(e) { console.log(e.nativeEvent); // the underlying browser Event } - As of React 17, synthetic events are no longer pooled and reused: each event object is freshly created and stays valid for as long as you hold a reference to it, even after the handler returns. Calling
e.persist()is now a no-op kept only for backward compatibility — you never need to call it.// React 16 and earlier nulled out event fields after the handler ran, // so reading e.target.value async required e.persist() first. // React 17+: the event object is never pooled, so this just works. function handleChange(e) { setTimeout(() => console.log(e.target.value), 1000); } - Get input values with e.target.value.
const handleChange = (e) => { setName(e.target.value); }; - Read checkbox state with e.target.checked.
function Checkbox() { const [checked, setChecked] = useState(false); return ( <input type="checkbox" checked={checked} onChange={(e) => setChecked(e.target.checked)} /> ); } - Get the key pressed from keyboard events with e.key.
function handleKeyDown(e) { console.log(e.key); // "Enter", "Escape", "ArrowUp" console.log(e.code); // "Enter", "Escape", "ArrowUp" console.log(e.ctrlKey); // true if Ctrl held } - Access the current target when events bubble up.
function handleClick(e) { console.log(e.target); // element that was clicked console.log(e.currentTarget); // element with the handler }
React Event Handling
(continued)Controlled vs Uncontrolled Components
- Controlled components: React manages the input value.
function TextInput() { const [value, setValue] = useState(""); return ( <input value={value} onChange={(e) => setValue(e.target.value)} /> ); } - Uncontrolled components: DOM manages the input.
function TextInput() { const inputRef = useRef(); function handleSubmit() { console.log(inputRef.current.value); } return ( <> <input ref={inputRef} /> <button onClick={handleSubmit}>Submit</button> </> ); } - Use defaultValue for uncontrolled inputs with an initial value.
// defaultValue sets the initial value but doesn't control updates <input defaultValue="Alice" ref={inputRef} /> - Controlled inputs enable real-time validation and formatting.
function PhoneInput() { const [phone, setPhone] = useState(""); const handleChange = (e) => { // Format as user types: 555-123-4567 const digits = e.target.value.replace(/\D/g, ""); setPhone(digits.replace(/(\d{3})(\d{3})(\d{4})/, "$1-$2-$3")); }; return <input value={phone} onChange={handleChange} />; } - Prefer controlled components for forms that need validation.
// Controlled: easy to read and validate before submit const isValid = email.includes("@") && password.length >= 8; <button disabled={!isValid}>Submit</button>
React Event Handling
(continued)Event Delegation
- Use event delegation for lists of items.
function List({ items }) { const handleItemClick = (e) => { if (e.target.dataset.id) { console.log("Item clicked:", e.target.dataset.id); } }; return ( <ul onClick={handleItemClick}> {items.map(item => ( <li key={item.id} data-id={item.id}> {item.name} </li> ))} </ul> ); } - More efficient than adding listeners to each item.
- Use data-* attributes to identify clicked elements.
- Use e.target.closest() to find the nearest matching ancestor.
function handleClick(e) { const row = e.target.closest("[data-row-id]"); if (row) { console.log("Row clicked:", row.dataset.rowId); } } - Stop bubbling with e.stopPropagation() to prevent parent handlers.
function Card() { return ( <div onClick={handleCardClick}> Card content <button onClick={(e) => { e.stopPropagation(); // don't trigger handleCardClick handleDeleteClick(); }}> Delete </button> </div> ); }
React Event Handling
(FAQ)FAQ
You're likely calling the function instead of passing a reference — write onClick={handleClick} not onClick={handleClick()}. If you need to pass arguments, wrap it in an arrow function: onClick={() => handleClick(id)}.
Use e.target.value inside your handler function, e.g. onChange={(e) => setValue(e.target.value)}. For checkboxes, use e.target.checked instead.
preventDefault stops the browser's default behavior (like a form submitting or a link navigating), while stopPropagation stops the event from bubbling up to parent elements. You often need both independently depending on what you're trying to block.
Uncontrolled components (using ref) are useful for file inputs or when integrating with non-React code, since file inputs can't be controlled by React state. For most form fields, controlled components give you real-time access to values for validation and conditional logic.
Add a name attribute to each input and use e.target.name in one handler to update the correct state key: setState(prev => ({ ...prev, [e.target.name]: e.target.value })). This avoids writing a separate handler for every field.
React Forms Handling
Build and manage controlled forms in React with inputs, validation, and submit handlers.
TL;DR
- 01Create controlled inputs by storing value in state.
- 02Update state on every keystroke with onChange handlers.
- 03Validate and submit forms with a form submit event.
Tips
- 01Use the <code>name</code> attribute on inputs to build a generic change handler that works for any number of fields without repetition.
Warnings
- 01Always call <code>e.preventDefault()</code> in the submit handler to stop the browser from reloading the page.
React Forms Handling
(continued)Controlled Inputs
- Create a controlled input by storing the value in state.
const [email, setEmail] = useState(""); <input value={email} onChange={(e) => setEmail(e.target.value)} /> - The input value stays in sync with the state at all times.
- React is the single source of truth for the input's value.
- Every keystroke updates state, which updates the input display.
- Controlled inputs let you validate and transform input before storing.
React Forms Handling
(continued)Multiple Inputs
- Store form data in a single state object to keep it organized.
const [form, setForm] = useState({ name: "", email: "" }); const handleChange = (e) => { const { name, value } = e.target; setForm(prev => ({ ...prev, [name]: value })); }; - Use the
nameattribute to identify which input changed. - Spread the previous state and update only the changed field.
- This pattern scales well as forms grow with more fields.
- Consider moving to a form library for very large forms.
React Forms Handling
(continued)Checkboxes and Selects
- For checkboxes, use
checkedinstead ofvaluein state.const [agree, setAgree] = useState(false); <input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} /> - For select dropdowns, store the selected value in state.
const [role, setRole] = useState("user"); <select value={role} onChange={(e) => setRole(e.target.value)}> <option value="user">User</option> <option value="admin">Admin</option> </select> - Handle radio buttons like checkboxes but with a group of options.
- Textareas work like text inputs with the
valueattribute.
React Forms Handling
(continued)Validation
- Validate input on every change or only when the user leaves the field.
const [email, setEmail] = useState(""); const [error, setError] = useState(""); const handleChange = (e) => { const value = e.target.value; setEmail(value); if (!value.includes("@")) setError("Invalid email"); else setError(""); }; - Show validation errors near the invalid input for quick feedback.
- Disable the submit button while there are validation errors.
<button disabled={error}>Submit</button> - Use a form library like React Hook Form for complex validation rules.
React Forms Handling
(continued)Form Submission
- Prevent the default form submission and handle it with JavaScript.
const handleSubmit = (e) => { e.preventDefault(); console.log(form); }; <form onSubmit={handleSubmit}> {/* inputs here */} <button type="submit">Submit</button> </form> - Access form data from state and send it to an API.
- Show loading and success states while submitting.
- Reset the form after successful submission using
setForm({}). - Handle errors gracefully and display them to the user.
React Forms Handling
(FAQ)FAQ
Add a name attribute to each input that matches the corresponding state key, then use a single handler: setState(prev => ({ ...prev, [e.target.name]: e.target.value })). This one function scales to any number of fields.
You're missing e.preventDefault() at the top of your submit handler. Without it, the browser performs its default form submission, which reloads the page and wipes your component state.
Checkboxes use e.target.checked instead of e.target.value. In a shared handler, check e.target.type === 'checkbox' and use the appropriate property: type === 'checkbox' ? e.target.checked : e.target.value.
Validate on submit for simple forms to avoid showing errors before the user finishes typing. For better UX, validate on onBlur (when the field loses focus) so errors appear after the user leaves a field rather than mid-input.
A controlled input stores its value in React state and reads it back via the value prop, making React the single source of truth. An uncontrolled input stores its value in the DOM and is read via a ref; controlled inputs are preferred because they make validation and dynamic updates straightforward.
React Fragment and Keys
Master React.Fragment, short syntax, and when keys are necessary.
TL;DR
- 01Use Fragment to group elements without adding a wrapper div.
- 02Use the <></> short syntax for cleaner code.
- 03Fragments cannot have keys unless using React.Fragment explicitly.
Tips
- 01Use the <></> short syntax by default for cleaner code, but switch to React.Fragment when you need the key prop.
Warnings
- 01Fragments don't support className, id, or other attributes — use a div if you need to style the wrapper.
React Fragment and Keys
(continued)Fragment Basics
- Use React.Fragment to group multiple elements without a wrapper.
function List() { return ( <React.Fragment> <h2>Title</h2> <p>Description</p> </React.Fragment> ); } - Fragments don't create extra DOM nodes.
// Without Fragment: creates a div <div> <Header /> <Content /> <Footer /> </div> // With Fragment: no extra node <React.Fragment> <Header /> <Content /> <Footer /> </React.Fragment> - Return multiple elements from a component.
function Profile() { return ( <React.Fragment> <UserHeader /> <UserContent /> <UserFooter /> </React.Fragment> ); } - Use fragments in table rows to avoid invalid HTML.
function TableRow({ item }) { return ( <React.Fragment> <td>{item.name}</td> <td>{item.price}</td> <td>{item.stock}</td> </React.Fragment> ); } - Fragments are required when a component must return multiple siblings.
// JSX requires a single root — Fragment is the clean solution function NavItems() { return ( <> <li><a href="/home">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/contact">Contact</a></li> </> ); }
React Fragment and Keys
(continued)Short Syntax
- Use <></> as shorthand for React.Fragment.
function List() { return ( <> <h2>Title</h2> <p>Description</p> </> ); } - Short syntax is cleaner and more readable.
- Most projects use <></> instead of React.Fragment.
// Preferred: short syntax <> <Component /> </> // Also works: full syntax <React.Fragment> <Component /> </React.Fragment> - Short syntax works in any JSX context.
function App() { return ( <main> <> <Sidebar /> <Content /> </> </main> ); } - Use <></> for conditional rendering of multiple elements.
function Status({ isLoggedIn }) { return isLoggedIn ? ( <> <WelcomeMessage /> <UserMenu /> </> ) : ( <> <LoginPrompt /> <SignupLink /> </> ); }
React Fragment and Keys
(continued)Fragment with Keys
- Only React.Fragment supports the key prop.
function List({ items }) { return ( <> {items.map(item => ( <React.Fragment key={item.id}> <dt>{item.name}</dt> <dd>{item.description}</dd> </React.Fragment> ))} </> ); } - Use keys when rendering multiple elements per item.
// Without key: React can't track which elements belong together items.map(item => ( <React.Fragment key={item.id}> <Header title={item.title} /> <Body content={item.content} /> </React.Fragment> )) - Short syntax <></> doesn't support keys.
// This won't work: <> {items.map(item => ( <> <p key={item.id}>{item.name}</p> </> ))} </> - Place the key on React.Fragment, not on child elements.
// Wrong: key on child, not on fragment {items.map(item => ( <React.Fragment> <p key={item.id}>{item.name}</p> </React.Fragment> ))} // Correct: key on the fragment itself {items.map(item => ( <React.Fragment key={item.id}> <p>{item.name}</p> </React.Fragment> ))} - Keys must be unique among siblings, not globally unique.
// These keys are fine — they're in separate lists listA.map(item => <React.Fragment key={item.id}>...</React.Fragment>) listB.map(item => <React.Fragment key={item.id}>...</React.Fragment>)
React Fragment and Keys
(continued)Use Cases
- Return multiple elements from a component.
function DialogBox() { return ( <> <Modal.Header>Confirm Action</Modal.Header> <Modal.Body>Are you sure?</Modal.Body> <Modal.Footer> <button>Cancel</button> <button>Confirm</button> </Modal.Footer> </> ); } - Wrap elements for styling without extra divs.
return ( <> <style>{` span { color: red; } `}</style> <span>Styled text</span> </> ); - Group related elements in definition lists.
function DefinitionList({ terms }) { return ( <dl> {terms.map(term => ( <React.Fragment key={term.id}> <dt>{term.word}</dt> <dd>{term.definition}</dd> </React.Fragment> ))} </dl> ); } - Group table cells when building flexible table row components.
function ProductRow({ product }) { return ( <tr> <React.Fragment> <td>{product.name}</td> <td>{product.category}</td> <td>${product.price}</td> </React.Fragment> </tr> ); } - Avoid invalid HTML nesting by wrapping list items in fragments.
function NavGroup({ label, links }) { return ( <> <li className="group-label">{label}</li> {links.map(link => ( <li key={link.href}> <a href={link.href}>{link.text}</a> </li> ))} </> ); }
React Fragment and Keys
(continued)Common Mistakes
- Don't use <></> when you need a key on items.
// Wrong: loses key benefit {items.map(item => ( <> <p key={item.id}>{item.name}</p> </> ))} // Correct: use React.Fragment with key {items.map(item => ( <React.Fragment key={item.id}> <p>{item.name}</p> </React.Fragment> ))} - Don't add classes or other attributes to fragments.
// Wrong: fragments don't support attributes <> className="wrapper"> <Content /> </> // Correct: use a div if you need attributes <div className="wrapper"> <Content /> </div> - Don't forget to import React when using the full syntax.
// Required for React.Fragment in older setups import React from "react"; // Not required for short syntax — handled by JSX transform <>...</> - Don't use index as key if items can be reordered or removed.
// Bad: index as key causes issues when list order changes {items.map((item, index) => ( <React.Fragment key={index}>...</React.Fragment> ))} // Good: stable unique ID {items.map(item => ( <React.Fragment key={item.id}>...</React.Fragment> ))} - Don't nest fragments unnecessarily — keep JSX clean.
// Unnecessary nesting <> <> <p>Hello</p> </> </> // Clean: single fragment <> <p>Hello</p> </>
React Fragment and Keys
(FAQ)FAQ
Use React.Fragment when you need to pass a key prop, such as when rendering a list of fragments. The short syntax <></> doesn't accept any props, so explicit React.Fragment is the only option in those cases.
No, Fragments don't support any DOM attributes like className, id, or style. If you need to style the wrapper element, use a div or another semantic HTML element instead.
Map over your data and return React.Fragment with a key prop for each item: items.map(item => <React.Fragment key={item.id}>...</React.Fragment>). This avoids wrapper divs while satisfying React's key requirement for lists.
No, Fragments are invisible in the DOM — they leave no trace in the rendered HTML. Your flex or grid container will see the Fragment's children as direct children, which is exactly why Fragments are useful for avoiding layout-breaking wrapper divs.
Fragments themselves don't affect render order, so if children appear out of order the issue is elsewhere — typically conditional rendering logic or an unsorted data source. Check the order of expressions inside your Fragment and the data being mapped.
React Keys and Lists
Render lists efficiently with the key prop, understand reconciliation, and avoid common list rendering bugs.
TL;DR
- 01Always provide a key prop when rendering lists of items.
- 02Use stable, unique identifiers — not array indices.
- 03Keys help React identify which items changed for efficient updates.
Tips
- 01Always use a unique identifier from your data as the key, preferably an ID from a database or UUID library.
Warnings
- 01Using array indices as keys causes bugs when lists are reordered, filtered, or items are added/removed dynamically.
React Keys and Lists
(continued)Why Keys Matter
- Keys help React identify which items have changed, been added, or removed.
const items = [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" } ]; {items.map(item => ( <div key={item.id}>{item.name}</div> ))} - Without keys, React assumes items are in the same position.
- This causes state to persist incorrectly between items.
// Without keys: input state follows the position, not the person {people.map((person, index) => ( <div key={index}> <input defaultValue={person.name} /> </div> ))} - Keys ensure component state stays with the correct item.
React Keys and Lists
(continued)Choosing Good Keys
- Use stable, unique identifiers from your data.
// Good: unique ID {items.map(item => ( <div key={item.id}>{item.name}</div> ))} - Avoid array indices as keys, especially for dynamic lists.
// Bad: index changes when items are reordered {items.map((item, index) => ( <div key={index}>{item.name}</div> ))} - Use IDs from a database or UUID library for reliable keys.
import { v4 as uuidv4 } from "uuid"; const newItem = { id: uuidv4(), name: "Charlie" }; - Keys don't need to be globally unique, just unique within the list.
React Keys and Lists
(continued)Common Mistakes
- Don't use array indices when the list can be reordered.
// Problem: if items are sorted, indices change const sorted = items.sort((a, b) => a.name.localeCompare(b.name)); sorted.map((item, i) => <Item key={i} />); // Bad! - Don't generate keys on the fly with random values.
// Bad: new key generated on every render items.map(item => ( <div key={Math.random()}>{item.name}</div> )) - Don't use complex objects as keys — use simple strings or numbers.
// Bad: object reference changes {items.map(item => ( <div key={item}>{item.name}</div> ))} - Always provide a key prop when rendering with map.
React Keys and Lists
(continued)List Rendering Patterns
- Render a simple list with stable keys.
export default function UserList({ users }) { return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } - Handle empty lists gracefully.
if (users.length === 0) { return <p>No users found</p>; } return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); - Filter and sort before rendering, not in the map.
const active = users.filter(u => u.isActive); const sorted = active.sort((a, b) => a.name.localeCompare(b.name)); return sorted.map(user => ( <UserItem key={user.id} user={user} /> ));
React Keys and Lists
(continued)Performance Considerations
- Keys allow React to reuse DOM elements and preserve component state.
// With proper keys, React updates only changed items {items.map(item => ( <Item key={item.id} data={item} /> ))} - Avoid creating new components inside map — extract to separate component.
// Bad: defines component inside map items.map(item => { const Component = createComponent(item); return <Component key={item.id} />; }) // Good: use a wrapper component items.map(item => ( <ItemWrapper key={item.id} item={item} /> )) - Use React.memo on list items to avoid unnecessary re-renders.
const ListItem = React.memo(({ item }) => ( <div>{item.name}</div> )); items.map(item => ( <ListItem key={item.id} item={item} /> ))
React Keys and Lists
(FAQ)FAQ
React uses keys to track which items changed, were added, or removed between renders. Without keys, React falls back to index-based diffing, which can cause incorrect component reuse and state bugs when the list changes.
No — keys generated at render time (like Math.random()) change every render, forcing React to unmount and remount every list item each time. This destroys component state and tanks performance.
An index-based key shifts when items are reordered or deleted, causing React to match the wrong component to the wrong data. A stable ID (like a database primary key) always points to the same item regardless of position.
No — keys only need to be unique among siblings in the same list. The same key value can appear in a completely different list without conflict.
Use a library like uuid or nanoid to assign a stable ID when the item is first created (e.g., on user input), and store that ID with the data. Never generate the key inline during render — generate it once and persist it.
React Props and Children
Master prop drilling, children, and patterns for composable component APIs.
TL;DR
- 01Pass data to children using props in a parent component.
- 02Use children to accept JSX from parent components.
- 03Avoid prop drilling by using Context or composition patterns.
Tips
- 01Use composition and Context instead of drilling props through many levels — it makes code cleaner and easier to maintain.
Warnings
- 01Don't pass too many props to a single component — it's a sign to break it into smaller, more focused components.
React Props and Children
(continued)Basic Props
- Pass data to child components using props as an object.
function Greeting({ name, age }) { return <p>Hello {name}, age {age}</p>; } <Greeting name="Alice" age={30} /> - Destructure props for cleaner code in function components.
function Button({ label, onClick, disabled = false }) { return ( <button onClick={onClick} disabled={disabled}> {label} </button> ); } - Use default values for optional props.
function Card({ title = "Untitled", content }) { return <div><h2>{title}</h2><p>{content}</p></div>; } - Pass functions as props to handle events from children.
<Button onClick={() => handleClick()} label="Click me" />
React Props and Children
(continued)Using Children
- Accept JSX as children to create flexible wrapper components.
function Card({ children, title }) { return ( <div className="card"> <h2>{title}</h2> <div className="content">{children}</div> </div> ); } <Card title="Welcome"> <p>This is card content</p> </Card> - Children is a special prop that contains nested JSX.
- Multiple children are passed automatically as an array.
<Container> <Header /> <Main /> <Footer /> </Container> - Use React.Children.map to iterate over children.
function Row({ children }) { return ( <tr> {React.Children.map(children, (child, index) => ( <td key={index}>{child}</td> ))} </tr> ); }
React Props and Children
(continued)Prop Drilling and Solutions
- Prop drilling occurs when passing props through many levels.
// Level 1 <App user={user}> // Level 2 <Layout user={user}> // Level 3 <Sidebar user={user}> // Level 4 <Profile user={user} /> </Sidebar> </Layout> </App> - Use Context to avoid prop drilling for shared data.
const UserContext = createContext(); <UserContext.Provider value={user}> <App /> </UserContext.Provider> function Profile() { const user = useContext(UserContext); return <div>{user.name}</div>; } - Use composition to pass components instead of data.
<Layout sidebar={<Sidebar />}> <Main /> </Layout>
React Props and Children
(continued)Component Composition Patterns
- Build flexible layouts by accepting components as props.
function Page({ header: Header, content: Content, footer: Footer }) { return ( <> <Header /> <Content /> <Footer /> </> ); } <Page header={<Header />} content={<MainContent />} footer={<Footer />} /> - Use the render prop pattern for dynamic content.
function DataFetcher({ render }) { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); }, []); return render(data); } <DataFetcher render={(data) => <List items={data} />} /> - Compose small, focused components for reusability.
<Modal> <Modal.Header>Title</Modal.Header> <Modal.Body>Content</Modal.Body> <Modal.Footer>Actions</Modal.Footer> </Modal>
React Props and Children
(continued)Advanced Patterns
- Clone and modify children with cloneElement.
function FormFields({ children, error }) { return React.Children.map(children, (child) => React.cloneElement(child, { error }) ); } - Use children as a function for render props.
function Toggle({ children }) { const [open, setOpen] = useState(false); return children({ open, toggle: () => setOpen(!open) }); } <Toggle> {({ open, toggle }) => ( <button onClick={toggle}> {open ? "Close" : "Open"} </button> )} </Toggle> - Spread remaining props to avoid manual forwarding.
function Button({ label, ...rest }) { return <button {...rest}>{label}</button>; }
React Props and Children
(FAQ)FAQ
Define the function in the parent component and pass it like any other prop: <Button onClick={handleClick} />. In the child, call it via props.onClick() or destructure it directly.
props.children is implicitly populated by JSX between opening and closing tags, while a named slot prop (e.g., header={<Header />}) must be passed explicitly as an attribute. Use named slots when you need multiple distinct content areas in a component.
Use React Context to provide values deep in the tree without threading props through intermediate components, or restructure with composition — pass the deeply-needed component directly as a prop or children so intermediaries never need to know about it.
Yes: <Component {...props} /> works, but be selective — spreading unknown props onto DOM elements causes warnings and can expose internal state. Destructure what you need and spread only a known rest object, e.g., const { internal, ...rest } = props.
Use destructuring defaults in the function signature: function Card({ title = 'Untitled', size = 'md' }). This is preferred over the legacy Component.defaultProps, which is deprecated in modern React.
React useRef Hook
Learn useRef fundamentals: DOM access, callback refs, useImperativeHandle, and React 19 ref-as-prop.
TL;DR
- 01useRef persists a .current value across renders without re-rendering.
- 02React 19 passes ref as a prop — forwardRef is deprecated.
- 03useImperativeHandle lets a child component expose a controlled API.
Tips
- 01Initialise useRef with the type of value you expect. Pass null for DOM refs and a real initial value (e.g. 0, false) for mutable value refs — this makes TypeScript inference easier.
Warnings
- 01A ref mutation inside render is allowed only for the renders counter pattern shown above where you don't use the value for display. Any ref read that feeds JSX output must happen in an effect.
React useRef Hook
(continued)useRef Basics
useRef creates a plain JavaScript object with a single .current property. React keeps the same object across every render — mutating .current never schedules a re-render.
- Attach a ref to a DOM element to call browser methods on it.
import { useRef } from 'react'; function TextInput() { const inputRef = useRef(null); function handleClick() { inputRef.current.focus(); // direct DOM call } return ( <> <input ref={inputRef} placeholder="Type here" /> <button onClick={handleClick}>Focus input</button> </> ); } - Store any mutable value — not just DOM nodes. Here a render counter persists without triggering re-renders.
function RenderCounter() { const renders = useRef(0); renders.current += 1; // mutate during render is fine for refs return <p>Rendered {renders.current} times (no re-render loop)</p>; } - Key distinction: ref vs state.
- ref.current changes — component does not re-render, UI stays the same.
- setState changes — component re-renders, UI updates.
React useRef Hook
(continued)React 19: Refs as Props
Before React 19, passing a ref into a child component required wrapping it in forwardRef. React 19 removes that requirement — ref is now a plain prop, just like className or onClick.
- Before (React 18 and earlier) — forwardRef wrapper required:
// React 18: you had to wrap in forwardRef const Input = forwardRef(function Input(props, ref) { return <input ref={ref} {...props} />; }); function Form() { const inputRef = useRef(null); return <Input ref={inputRef} placeholder="Email" />; } - After (React 19) — ref arrives as a plain prop:
// React 19: ref is just a prop function Input({ ref, ...props }) { return <input ref={ref} {...props} />; } function Form() { const inputRef = useRef(null); return <Input ref={inputRef} placeholder="Email" />; } - Class components still use
createRefand are unaffected by this change. - forwardRef still works in React 19 for library backward compatibility but shows a deprecation warning in dev mode.
React useRef Hook
(continued)Callback Refs
Instead of passing a ref object to the ref prop, you can pass a function. React calls it with the DOM node on mount and null on unmount. This is useful when you need to act on the node the instant it appears — before any effect would fire.
- Measure an element's size the moment it mounts:
function MeasuredBox() { const [height, setHeight] = useState(null); const measuredRef = useCallback((node) => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []); // stable function — no deps needed return ( <> <div ref={measuredRef} style={{ padding: 20 }}> Resize me </div> {height !== null && <p>Height: {height}px</p>} </> ); } - The callback fires again when the element re-mounts (e.g. after conditional rendering), giving you an automatic measurement update — a regular ref object would not trigger this.
- Wrap the callback in
useCallbackwith a stable dependency array to avoid React re-running the ref function on every render. - Clean-up pattern — run code on both attach and detach:
const logRef = useCallback((node) => { if (node) { console.log('mounted:', node.tagName); } else { console.log('unmounted'); } }, []); return <div ref={logRef}>Watch the console</div>;
React useRef Hook
(continued)useImperativeHandle
useImperativeHandle lets a child component decide exactly which methods a parent can call through a ref. Instead of exposing the raw DOM node, the child publishes a controlled object.
- Expose
focusandclearwithout leaking the internal input ref:// React 19 style — ref as a prop, no forwardRef needed function FancyInput({ ref }) { const inputRef = useRef(null); useImperativeHandle(ref, () => ({ focus() { inputRef.current.focus(); }, clear() { inputRef.current.value = ''; }, })); return <input ref={inputRef} className="fancy" />; } function Form() { const fancyRef = useRef(null); return ( <> <FancyInput ref={fancyRef} /> <button onClick={() => fancyRef.current.focus()}>Focus</button> <button onClick={() => fancyRef.current.clear()}>Clear</button> </> ); } - Pass a dependency array as the third argument to control when the handle object is recreated:
useImperativeHandle(ref, () => ({ getValue: () => value }), [value]); - Restrict the exposed API intentionally — if you don't expose
scrollIntoView, the parent simply cannot call it.useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), // scrollIntoView intentionally not exposed }));
React useRef Hook
(continued)Best Practices
- Prefer state over refs for UI values. If a value change should update the screen, use
useState— not a ref.// Wrong: ref won't update the UI const count = useRef(0); const increment = () => { count.current++; }; // no re-render // Right: state drives the UI const [count, setCount] = useState(0); const increment = () => setCount(c => c + 1); - Never read refs during rendering. The DOM node is null until after mount. Reading it during render returns null at best and throws at worst.
// Wrong: ref is null here function Bad() { const ref = useRef(null); const width = ref.current?.offsetWidth; // always undefined on first render return <div ref={ref}>{width}px</div>; } // Right: read in effect or event handler function Good() { const ref = useRef(null); const [width, setWidth] = useState(0); useEffect(() => { setWidth(ref.current.offsetWidth); }, []); return <div ref={ref}>{width}px</div>; } - React 19: drop forwardRef in new components. Accept ref as a plain prop — it's simpler and the JSX tools generate better TypeScript types automatically.
- Use callback refs for dynamic elements. When an element may mount and unmount (e.g. behind a conditional), a callback ref ensures you always run your setup code at the right time.
- Don't store derived data in refs. Compute it during render instead — refs that shadow state become stale and are hard to debug.
// Wrong: keeping a ref in sync with state manually const doubleRef = useRef(count * 2); // Right: derive during render const double = count * 2;
React useRef Hook
(FAQ)FAQ
useState triggers a re-render when its value changes; useRef does not. Use useState when a value change needs to update the UI. Use useRef when you need to hold onto a DOM node, a timer ID, or any value that should survive re-renders without causing them.
No. In React 19, function components receive ref as a plain prop, so you can write function Input({ ref }) {} directly. forwardRef still works for backward compatibility but is considered legacy and may be removed in a future major version.
A callback ref is a function you pass to the ref prop instead of a ref object. React calls it with the DOM node on mount and with null on unmount. Use it when you need to imperatively measure an element or set up an observer as soon as the node becomes available, especially for dynamically rendered elements.
Use useImperativeHandle when a parent needs to call specific methods on a child component — for example, focus(), reset(), or scroll() — but you want to hide the child's internal DOM node. It lets you publish a minimal, intentional API instead of exposing the raw DOM element.
Refs attached to JSX elements are populated after the component mounts, so ref.current is null during the initial render. Always read ref.current inside a useEffect (which runs after mount) or inside an event handler (which fires after the user interacts with an already-mounted element).
React useState Hook
Manage React component state with useState to store and update values across renders.
TL;DR
- 01Declare state with useState and get back value and setter.
- 02Call setter to update state, triggering a re-render.
- 03Use functional updates for state based on previous state.
Tips
- 01Use functional updates (prev => ...) when the new state depends on the old state — it's safer and more readable.
Warnings
- 01Never mutate state directly — always create new objects and arrays to trigger re-renders properly.
React useState Hook
(continued)Basic useState
- Declare state with initial value.
const [count, setCount] = useState(0); - count is the current value, setCount updates and re-renders.
function Counter() { const [count, setCount] = useState(0); return ( <> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}> Increment </button> </> ); } - Each state variable needs its own useState call.
- Initialize state with strings, numbers, booleans, arrays, or objects.
const [name, setName] = useState(""); const [isOpen, setIsOpen] = useState(false); const [items, setItems] = useState([]); const [user, setUser] = useState(null); - Calling the setter always replaces the value, never merges.
// Calling setCount replaces the entire value setCount(42); // count is now 42, not 42 + oldCount
React useState Hook
(continued)Multiple State Variables
- Declare multiple state variables separately.
function Form() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [age, setAge] = useState(0); return ( <> <input value={name} onChange={(e) => setName(e.target.value)} /> <input value={email} onChange={(e) => setEmail(e.target.value)} /> <input value={age} onChange={(e) => setAge(e.target.value)} /> </> ); } - Group related state into a single object.
const [form, setForm] = useState({ name: "", email: "", age: 0 }); - Update a single field in an object with spread syntax.
const handleChange = (field) => (e) => { setForm(prev => ({ ...prev, [field]: e.target.value })); }; - Keep unrelated state variables separate for clarity.
// These are unrelated — keep them separate const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); - Use useReducer when multiple variables always update together.
// If isLoading, error, and data always change at once // consider useReducer instead of three useState calls
React useState Hook
(continued)Functional Updates
- Use a function to update state based on previous value.
function Counter() { const [count, setCount] = useState(0); // Direct update (fine for simple cases) // setCount(count + 1); // Functional update (preferred for dependent updates) return <button onClick={() => setCount(prev => prev + 1)}>Increment</button>; } - Functional updates are safer when multiple updates happen fast.
const handleClick = () => { setCount(prev => prev + 1); setCount(prev => prev + 1); // Both increments use the latest state — count increases by 2 }; - Use functional updates inside useEffect and useCallback.
useEffect(() => { const timer = setInterval(() => { setCount(prev => prev + 1); // safe inside interval }, 1000); return () => clearInterval(timer); }, []); // no count in deps needed - Functional updates work with arrays too.
function addItem(newItem) { setItems(prev => [...prev, newItem]); } function removeItem(id) { setItems(prev => prev.filter(item => item.id !== id)); } - Direct updates are fine when the new value doesn't depend on old state.
// setName doesn't need the previous name setName("Alice"); setIsOpen(true);
React useState Hook
(continued)Lazy Initialization
- Initialize state from an expensive computation using a function.
const [state, setState] = useState(() => { return expensiveComputation(); }); - Function runs only on mount, not on every render.
const [todos, setTodos] = useState(() => { const saved = localStorage.getItem("todos"); return saved ? JSON.parse(saved) : []; }); - Useful for loading from localStorage or parsing initial data.
- Without lazy initialization, the function runs on every render.
// Bad: expensiveComputation() runs on every render const [state, setState] = useState(expensiveComputation()); // Good: arrow function defers the call to mount only const [state, setState] = useState(() => expensiveComputation()); - Pass the function itself, not the result of calling it.
// Correct: pass a function useState(() => computeInitialValue()) // Wrong: calls the function immediately useState(computeInitialValue())
React useState Hook
(continued)Updating Objects and Arrays
- Create a new object when updating — never mutate state directly.
const [user, setUser] = useState({ name: "Alice", age: 30 }); // Wrong: mutation — React won't detect the change user.age = 31; // Right: create new object with spread setUser({ ...user, age: 31 }); - Create a new array for all array state operations.
const [items, setItems] = useState(["a", "b", "c"]); // Add item setItems([...items, "d"]); // Remove item setItems(items.filter(item => item !== "b")); - Update a nested object by spreading at every level.
const [profile, setProfile] = useState({ name: "Alice", address: { city: "NYC" } }); // Update nested field setProfile(prev => ({ ...prev, address: { ...prev.address, city: "LA" } })); - Update a specific item in an array by mapping over it.
const [tasks, setTasks] = useState([{ id: 1, done: false }]); function toggleTask(id) { setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t) ); } - Use the Immer library for complex nested updates.
import produce from "immer"; setProfile(produce(draft => { draft.address.city = "LA"; // direct mutation is safe inside produce }));
React useState Hook
(FAQ)FAQ
Use functional updates whenever the new state depends on the previous state, especially inside event handlers, async callbacks, or effects where the closure may capture a stale value. This guarantees you're working with the latest state, not a snapshot from when the function was created.
Spread the existing object and override only the changed fields: setState(prev => ({ ...prev, name: 'new' })). React requires a new object reference to detect the change, so never do setState(obj.name = 'new') directly.
Yes — declare as many useState calls as you need, one per logical piece of state. Keeping state variables separate (e.g., const [name, setName] and const [age, setAge]) is cleaner and avoids the need to spread on every update compared to storing everything in one object.
Passing a function to useState — useState(() => expensiveCalc()) — tells React to call it only on the initial render instead of every render. Use this when the initial value requires heavy computation, parsing, or reading from localStorage.
Calling array.push() mutates the existing array, so React sees the same reference and skips the re-render. Instead, create a new array: setState(prev => [...prev, newItem]).
React Accessibility
Build accessible React apps using semantic HTML, ARIA attributes, and keyboard navigation.
TL;DR
- 01Use semantic HTML elements to give the browser meaningful structure.
- 02Add ARIA attributes only when semantic HTML is not enough.
- 03Test every interactive element with keyboard and screen readers.
Tips
- 01Use semantic HTML first — it covers 80% of accessibility requirements without any extra ARIA attributes or JavaScript.
Warnings
- 01ARIA is not a substitute for semantic HTML — use it to enhance, not replace proper HTML structure.
React Accessibility
(continued)Semantic HTML
- Use semantic elements instead of divs.
// Bad: divs with no meaning <div onClick={() => setOpen(!open)}>Menu</div> // Good: semantic button element <button onClick={() => setOpen(!open)}>Menu</button> - Use proper heading hierarchy to structure pages.
<h1>Main Title</h1> <section> <h2>Section Title</h2> <p>Content</p> </section> - Use landmark elements for page regions.
<header>Site Header</header> <nav>Navigation Links</nav> <main>Page Content</main> <footer>Site Footer</footer> - Use lists for groups of related items.
<ul> <li><a href="/about">About</a></li> <li><a href="/contact">Contact</a></li> </ul> - Use the button element for any clickable action.
// Button triggers an action — use <button> <button onClick={openModal}>Open Settings</button> // Link navigates — use <a> <a href="/profile">View Profile</a>
React Accessibility
(continued)ARIA Attributes
- Use aria-label when the visible text doesn't describe the element.
<button aria-label="Close menu">×</button> <button aria-label="Delete item Alice">Delete</button> - Use aria-live to announce dynamic content to screen readers.
<div aria-live="polite" aria-atomic="true"> {statusMessage} </div> - Use role to define the purpose of a custom element.
<div role="button" onClick={handleClick} tabIndex="0"> Custom Button </div> - Use aria-expanded to indicate open or closed state.
<button aria-expanded={isOpen} aria-controls="menu-list" onClick={() => setIsOpen(!isOpen)} > Menu </button> - Use aria-describedby to link help text to an input.
<input id="password" type="password" aria-describedby="password-hint" /> <p id="password-hint">Must be at least 8 characters.</p>
React Accessibility
(continued)Forms and Labels
- Link every input to a label using htmlFor and id.
<label htmlFor="email">Email:</label> <input id="email" type="email" /> - Use specific input types for built-in validation.
<input type="email" /> <input type="password" /> <input type="date" /> - Show validation errors with aria-invalid and aria-errormessage.
<input id="email" type="email" aria-invalid={!!error} aria-errormessage="email-error" /> {error && <p id="email-error" role="alert">{error}</p>} - Mark required fields with required and aria-required.
<label htmlFor="name">Name <span aria-hidden="true">*</span></label> <input id="name" type="text" required aria-required="true" /> - Use fieldset and legend to group related form controls.
<fieldset> <legend>Notification preferences</legend> <label><input type="checkbox" name="email" /> Email</label> <label><input type="checkbox" name="sms" /> SMS</label> </fieldset>
React Accessibility
(continued)Keyboard Navigation
- Make interactive elements keyboard accessible by default.
// Buttons and links are keyboard accessible by default <button onClick={handleClick}>Click me</button> <a href="/next">Next page</a> - Use tabIndex="0" to make custom elements focusable.
<div role="button" tabIndex="0" onClick={handleClick} onKeyDown={(e) => e.key === "Enter" && handleClick()} > Custom Button </div> - Handle both Enter and Space for custom button elements.
function handleKeyDown(e) { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleClick(); } } - Trap focus inside modals when they are open.
function Modal({ isOpen, onClose, children }) { const firstFocusRef = useRef(null); useEffect(() => { if (isOpen) firstFocusRef.current?.focus(); }, [isOpen]); return isOpen ? ( <div role="dialog" aria-modal="true"> <button ref={firstFocusRef} onClick={onClose}>Close</button> {children} </div> ) : null; } - Use tabIndex="-1" to remove elements from tab order.
// Remove decorative icon from keyboard navigation <span tabIndex="-1" aria-hidden="true">★</span>
React Accessibility
(continued)Screen Reader Testing
- Test with built-in screen readers on each platform.
macOS: VoiceOver (Cmd + F5) Windows: NVDA (free download at nvaccess.org) iOS: VoiceOver (Settings > Accessibility) Android: TalkBack (Settings > Accessibility) - Use aria-live for dynamic status messages.
<div aria-live="polite" aria-atomic="true"> {loadingMessage} </div> - Use role="alert" for urgent messages that need immediate attention.
{error && ( <div role="alert"> {error} </div> )} - Hide decorative elements from screen readers with aria-hidden.
<img src="decorative-bg.png" alt="" aria-hidden="true" /> <span aria-hidden="true">→</span> - Use jest-axe to automate accessibility checks in tests.
import { axe, toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations); test('has no accessibility violations', async () => { const { container } = render(<MyForm />); const results = await axe(container); expect(results).toHaveNoViolations(); });
React Accessibility
(FAQ)FAQ
Use ARIA roles only when you cannot use a native HTML element that provides the same semantics — for example, when building a custom dropdown or tab component with div elements. Native elements like button, nav, and input already carry the correct role, so adding redundant ARIA roles on them is unnecessary and can cause conflicts.
Pass an id prop to your input and a matching htmlFor prop (not for) to your label element. For custom components that wrap native inputs, forward the id down to the underlying input element so the browser can establish the association.
Divs are not focusable and not in the tab order by default, so keyboard users cannot reach or activate them. Replace the div with a button element, which gets focus, Enter/Space handling, and the correct role for free — or add tabIndex={0}, onKeyDown handling, and role='button' if you must use a non-semantic element.
Use NVDA (Windows) or VoiceOver (Mac, built-in) paired with your app running locally — navigate through every interactive element using Tab and arrow keys and confirm the announced text matches the visual label. The axe DevTools browser extension can also catch the majority of ARIA and labeling issues automatically during development.
Use aria-label to provide an inline string label when no visible text label exists, such as an icon-only button. Use aria-labelledby when a visible element already contains the label text — point it to that element's id so screen readers read existing content instead of duplicating it in code.
React Composition
Master React patterns like compound components, render props, and higher-order components for clean reusable patterns.
TL;DR
- 01Use compound components to create flexible component APIs.
- 02Use render props to share logic through children functions.
- 03Use higher-order components to wrap and enhance existing components.
Tips
- 01Use custom hooks instead of render props or HOCs for new code, since hooks are simpler and easier to understand.
Warnings
- 01Deeply nested HOCs create hard-to-debug code — prefer flat composition using multiple custom hooks instead.
React Composition
(continued)Compound Components
- Build components that work together as a group with shared state.
const Accordion = ({ children }) => { const [active, setActive] = useState(null); return ( <AccordionContext.Provider value={{ active, setActive }}> {children} </AccordionContext.Provider> ); }; const AccordionItem = ({ id, title, children }) => { const { active, setActive } = useContext(AccordionContext); return ( <div> <button onClick={() => setActive(id)}>{title}</button> {active === id && <div>{children}</div>} </div> ); }; <Accordion> <AccordionItem id="1" title="Section 1">Content 1</AccordionItem> <AccordionItem id="2" title="Section 2">Content 2</AccordionItem> </Accordion> - Compound components are flexible because the child components control their appearance.
- They share state through Context instead of passing props through every level.
- Use this pattern for tightly coupled components like form fields and labels.
React Composition
(continued)Render Props
- Pass a function as a prop to let child components decide what to render.
const MouseTracker = ({ render }) => { const [pos, setPos] = useState({ x: 0, y: 0 }); const handleMouseMove = (e) => { setPos({ x: e.clientX, y: e.clientY }); }; return ( <div onMouseMove={handleMouseMove}> {render(pos)} </div> ); }; <MouseTracker render={({ x, y }) => ( <p>Mouse at {x}, {y}</p> )} /> - The render function receives data and returns JSX to display.
- This lets you reuse logic without creating a wrapper component.
- Render props are flexible because the caller decides what to render.
- The
childrenfunction is a special render prop pattern.<MouseTracker> {({ x, y }) => <p>Position: {x}, {y}</p>} </MouseTracker>
React Composition
(continued)Higher-Order Components (HOC)
- Wrap a component to add or modify its behavior.
const withAuth = (Component) => { return (props) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { checkAuth().then(u => { setUser(u); setLoading(false); }); }, []); if (loading) return <p>Loading...</p>; if (!user) return <p>Not authenticated</p>; return <Component user={user} {...props} />; }; }; const Dashboard = ({ user }) => <h1>Welcome {user.name}</h1>; const ProtectedDashboard = withAuth(Dashboard); - HOCs add props, logic, or wrappers to existing components.
- They're useful for cross-cutting concerns like authentication or theming.
- Avoid deeply nested HOCs — use composition instead.
React Composition
(continued)When to Use Each Pattern
- Use compound components when building a cohesive component suite.
<Form> <Form.Field name="email" /> <Form.Field name="password" /> <Form.Submit>Login</Form.Submit> </Form> - Use render props to share logic between unrelated components.
<DataFetcher url="/api/users" render={data => ( <UserList users={data} /> )} /> - Use HOCs to enhance existing components with logic.
const enhancedComponent = withDataFetching(MyComponent); - Prefer composition over deeply nested patterns for readability.
React Composition
(continued)Modern Alternatives
- Custom hooks often replace render props and HOCs for sharing logic.
function useMousePosition() { const [pos, setPos] = useState({ x: 0, y: 0 }); useEffect(() => { const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY }); window.addEventListener('mousemove', handleMove); return () => window.removeEventListener('mousemove', handleMove); }, []); return pos; } function MyComponent() { const pos = useMousePosition(); return <p>Position: {pos.x}, {pos.y}</p>; } - Custom hooks are simpler and more flexible than render props or HOCs.
- Use hooks as your first choice for sharing logic in modern React.
- Composition patterns are still useful for component APIs and layout.
React Composition
(FAQ)FAQ
Use compound components when you want a flexible, declarative API where parent and child components share implicit state (like a Tabs/Tab pair). Use render props when you need to inject dynamic content or logic into a single component that consumers control directly.
Extract the logic into a custom hook and call it in each component that needs it. This is the modern recommended approach — it avoids wrapper components entirely and keeps the component tree flat.
An HOC wraps a component and returns a new one, adding behavior at the component level; it shows up in the React tree and can obscure the component hierarchy. A custom hook shares logic at the function level without adding any components to the tree, making it easier to trace and debug.
Wrapper hell happens when multiple HOCs are stacked around a single component, making the component tree and props difficult to follow. Refactor each HOC's logic into a separate custom hook and call them directly inside the component instead.
Use React context inside the compound component — the parent holds the shared state and provides it via a context, and each child reads from that context directly. This keeps the public API clean while avoiding the need to pass props through every intermediate child.
React Custom Hooks
Build reusable custom hooks to extract component logic and share stateful behavior.
TL;DR
- 01Extract stateful logic from components into reusable hook functions.
- 02Start every custom hook name with the word use.
- 03Call other hooks inside your hook to compose behavior.
Tips
- 01Return values from custom hooks in the same format as built-in hooks — use arrays for positional access or objects for named access.
Warnings
- 01Each component that uses a custom hook gets its own isolated state instance — they don't automatically share state between them.
React Custom Hooks
(continued)Basic Structure
- Write a custom hook as a regular function that calls React hooks.
function useToggle(initialValue = false) { const [value, setValue] = useState(initialValue); const toggle = useCallback(() => { setValue(v => !v); }, []); return [value, toggle]; } - Combine useState and useEffect to track a browser API value over time.
function useDebouncedValue(value, delayMs = 300) { const [debounced, setDebounced] = useState(value); useEffect(() => { const timer = setTimeout(() => setDebounced(value), delayMs); return () => clearTimeout(timer); // cancel if value changes again }, [value, delayMs]); return debounced; } - Call your hook from a component just like a built-in hook.
function SearchBox() { const [query, setQuery] = useState(''); const debouncedQuery = useDebouncedValue(query, 400); useEffect(() => { if (debouncedQuery) searchApi(debouncedQuery); }, [debouncedQuery]); return <input value={query} onChange={e => setQuery(e.target.value)} />; } - Keep each hook focused on a single concern.
React Custom Hooks
(continued)Naming Rules
- Always start custom hook names with use, like useToggle.
// Good: clearly indicates it's a hook function useToggle() { } function useFetch(url) { } function useLocalStorage(key) { } // Bad: doesn't follow naming convention function toggle() { } function fetch() { } - Use camelCase after the use prefix for consistency.
- Naming triggers React ESLint rules and warnings.
- Pick names that describe what the hook does.
React Custom Hooks
(continued)Common Patterns
- Build useToggle for managing boolean state.
function useToggle(initial = false) { const [value, setValue] = useState(initial); const toggle = () => setValue(!value); return [value, toggle]; } // Usage function Modal() { const [isOpen, toggleOpen] = useToggle(false); return ( <> <button onClick={toggleOpen}>Open</button> {isOpen && <div>Modal content</div>} </> ); } - Build usePrevious to track previous prop or state values.
function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }, [value]); return ref.current; }
React Custom Hooks
(continued)Composing and Typing Hooks
- Compose several built-in hooks inside one custom hook to build a higher-level abstraction.
function useOnlineStatus() { const [isOnline, setIsOnline] = useState(navigator.onLine); useEffect(() => { const goOnline = () => setIsOnline(true); const goOffline = () => setIsOnline(false); window.addEventListener('online', goOnline); window.addEventListener('offline', goOffline); return () => { window.removeEventListener('online', goOnline); window.removeEventListener('offline', goOffline); }; }, []); return isOnline; } - Build one custom hook on top of another instead of duplicating logic.
function useSyncedField(key, initialValue) { const [value, setValue] = useLocalStorage(key, initialValue); // reuse an existing hook const isOnline = useOnlineStatus(); // compose a second hook return { value, setValue, isOnline }; } - Type a generic custom hook so callers get the correct inferred return type.
function useToggle<T = boolean>(initial: T): [T, () => void] { const [value, setValue] = useState(initial); const toggle = useCallback(() => setValue(v => !v as T), []); return [value, toggle]; } const [isOpen, toggleOpen] = useToggle(false); // isOpen inferred as boolean - Test a custom hook in isolation with renderHook from @testing-library/react.
import { renderHook, act } from '@testing-library/react'; test('useToggle flips its value', () => { const { result } = renderHook(() => useToggle(false)); act(() => result.current[1]()); // call the toggle function expect(result.current[0]).toBe(true); }); - renderHook wraps your hook in a tiny test component so you never need a real UI to test it.
- Keep composed hooks shallow — two or three layers deep is easier to trace than a long chain.
React Custom Hooks
(continued)Sharing State Across Components
- Each component gets its own state instance of a custom hook.
function useCounter() { const [count, setCount] = useState(0); return [count, () => setCount(count + 1)]; } function Component1() { const [count, increment] = useCounter(); return <button onClick={increment}>{count}</button>; } function Component2() { const [count, increment] = useCounter(); return <button onClick={increment}>{count}</button>; } // Each component has separate count state - Use Context API with custom hooks to share state.
function useAuth() { const context = useContext(AuthContext); if (!context) throw new Error("useAuth needs provider"); return context; }
React Custom Hooks
(FAQ)FAQ
Extract into a custom hook when the same stateful logic (fetching, form handling, subscriptions) appears in multiple components, or when a single component's logic grows complex enough to benefit from separation. Custom hooks let you test and reuse that logic independently of any UI.
No — hooks must be called at the top level of a function, never inside conditionals, loops, or nested functions. This rule applies inside custom hooks too, not just components.
You can't share state via a hook alone — each component calling the hook gets its own independent state instance. To share state, lift it to a common parent and pass it down, or use a context provider that the hook reads from internally.
Return an array (like useState) when consumers will typically rename the values and order is obvious; return an object (like useReducer's dispatch pattern or React Query) when there are many return values or names carry meaning. Mixing both in a single hook adds confusion.
No — a custom hook runs in the context of the component that calls it, so re-render behavior is identical to inlining the logic. The hook abstraction has no runtime overhead.
React Error Boundaries
Catch React errors with error boundaries, handle lifecycle errors, and display fallback UI gracefully.
TL;DR
- 01Create a class component with getDerivedStateFromError to catch errors.
- 02Display fallback UI when errors are caught instead of a blank page.
- 03Log errors for debugging and monitoring in production.
Tips
- 01Use error boundaries at the page level and around features to keep the rest of your app working when something breaks.
Warnings
- 01Error boundaries only catch errors during rendering, not in event handlers or async code — use try-catch for those.
React Error Boundaries
(continued)Creating Error Boundaries
- Create a class component with error lifecycle methods.
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { return { hasError: true }; } render() { if (this.state.hasError) { return <h1>Something went wrong.</h1>; } return this.props.children; } } - Error boundaries only work with class components, not functions.
- Use
getDerivedStateFromErrorto update state when an error occurs. - This catches errors in the component tree below the boundary.
- Error boundaries must return a fallback UI or null.
React Error Boundaries
(continued)Logging and Debugging
- Use
componentDidCatchto log errors for debugging.class ErrorBoundary extends React.Component { componentDidCatch(error, errorInfo) { console.error("Error caught:", error); console.error("Error info:", errorInfo.componentStack); // Send to error tracking service logErrorToService(error, errorInfo); } render() { if (this.state.hasError) { return <h1>Something went wrong</h1>; } return this.props.children; } } componentDidCatchis called after an error has been thrown.- Use it to log errors to services like Sentry or DataDog.
- Include the error stack and component tree information in logs.
- Never throw errors from
componentDidCatchitself.
React Error Boundaries
(continued)What Error Boundaries Catch
- Error boundaries catch errors during rendering in child components.
// This error is caught function Child() { throw new Error("Oops"); } <ErrorBoundary> <Child /> {/* Error is caught here */} </ErrorBoundary> - They do NOT catch errors from event handlers or async code.
// This error is NOT caught <button onClick={() => { throw new Error("Oops"); }}> Click me </button> // Use try-catch for these instead <button onClick={() => { try { riskyOperation(); } catch (error) { // Handle error } }}> Safe click </button> - Error boundaries don't catch errors in the boundary itself.
React Error Boundaries
(continued)Nested Error Boundaries
- Use multiple error boundaries to granularly handle errors.
<ErrorBoundary> <Header /> <ErrorBoundary> <MainContent /> </ErrorBoundary> <Sidebar /> </ErrorBoundary> - Errors in MainContent are caught by the inner boundary.
- Errors in Header or Sidebar are caught by the outer boundary.
- This lets you keep the rest of the app running when part fails.
- Use granular boundaries for user-facing features like widgets.
React Error Boundaries
(continued)Error Boundary Patterns
- Show a user-friendly error message with recovery option.
class ErrorBoundary extends React.Component { render() { if (this.state.hasError) { return ( <div> <h1>Oops, something went wrong</h1> <p>Please try refreshing the page</p> <button onClick={() => window.location.reload()}> Reload Page </button> </div> ); } return this.props.children; } } - Create reusable error boundary components for common layouts.
<PageErrorBoundary> <PageContent /> </PageErrorBoundary> - Combine error boundaries with error tracking for production monitoring.
- Reset error state when user navigates to a new page.
React Error Boundaries
(FAQ)FAQ
Define a class component that implements getDerivedStateFromError to update state when an error is caught, then return fallback UI in render when that state is set. You can also implement componentDidCatch to receive the error and info objects for logging.
Error boundaries only intercept errors thrown during the React render cycle — event handlers run outside that cycle, so you need a try-catch block inside the handler itself.
Yes, and it's the recommended approach — nest them so a broken widget or feature only unmounts its own subtree while the rest of the page stays functional. Placing one at the root acts as a last-resort safety net, not a replacement for granular boundaries.
Implement componentDidCatch(error, info) in your class component and call your logging service there — info.componentStack gives you the full component trace, which is invaluable for debugging production issues.
No — as of React 18, error boundaries must be class components because the required lifecycle methods (getDerivedStateFromError, componentDidCatch) have no hook equivalents. Libraries like react-error-boundary wrap this pattern into a reusable component so you can use it declaratively without writing the class yourself.
React Hooks
Quick reference for core and advanced React hooks with usage rules and patterns.
TL;DR
- 01useState and useEffect cover most common state and side-effect needs.
- 02Use useCallback and useMemo only when you have a proven performance issue.
- 03Only call hooks at the top level and inside React function components.
Tips
- 01Create custom hooks to share logic between components — extract state and effects into reusable hooks with names starting with "use".
Warnings
- 01Always follow the Rules of Hooks — calling hooks conditionally or from non-component functions breaks React's internal state management.
React Hooks
(continued)Core Hooks
- useState for managing local component state.
const [count, setCount] = useState(0); return <button onClick={() => setCount(count + 1)}>{count}</button>; - useEffect for side effects and cleanup.
useEffect(() => { fetch("/api/data").then(r => r.json()).then(setData); return () => { /* cleanup */ }; }, []); - useContext for accessing shared data from a provider.
const { theme } = useContext(ThemeContext); return <div style={{ background: theme }}>Content</div>; - useReducer for state with multiple related actions.
const [state, dispatch] = useReducer(reducer, { count: 0 }); return <button onClick={() => dispatch({ type: "INCREMENT" })}> {state.count} </button>; - useRef for accessing DOM nodes or storing mutable values.
const inputRef = useRef(null); return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> </> );
React Hooks
(continued)Performance Hooks
- useCallback memoizes a function reference across renders.
const handleClick = useCallback(() => { doSomething(value); }, [value]); - useMemo memoizes a computed value across renders.
const expensiveValue = useMemo(() => { return computeExpensiveValue(a, b); }, [a, b]); - Use useCallback to prevent child re-renders when passing callbacks.
const handleSubmit = useCallback((data) => { saveData(data); }, []); // stable reference — MemoizedChild won't re-render return <MemoizedChild onSubmit={handleSubmit} />; - Use useMemo to avoid recalculating expensive derived data.
const sortedItems = useMemo( () => [...items].sort((a, b) => a.name.localeCompare(b.name)), [items] ); - Both hooks only optimize — they don't change behavior.
// Add these only after profiling shows a real bottleneck // Premature optimization adds complexity without benefit
React Hooks
(continued)Ref Hooks
- useRef creates a mutable ref that persists across renders.
const inputRef = useRef(null); return ( <> <input ref={inputRef} /> <button onClick={() => inputRef.current.focus()}>Focus</button> </> ); - useImperativeHandle exposes custom methods to parent components.
useImperativeHandle(ref, () => ({ focus: () => inputRef.current.focus(), clear: () => { inputRef.current.value = ""; } })); - Store mutable values that don't need to trigger re-renders.
const timerRef = useRef(null); function start() { timerRef.current = setInterval(tick, 1000); } function stop() { clearInterval(timerRef.current); } - Track previous prop or state value for comparisons.
const prevCountRef = useRef(count); useEffect(() => { prevCountRef.current = count; }, [count]); const prevCount = prevCountRef.current; // value from last render - Forward refs to child components with forwardRef.
const Input = forwardRef((props, ref) => ( <input ref={ref} {...props} /> ));
React Hooks
(continued)Advanced Hooks
- useLayoutEffect runs synchronously before paint — use for measurements.
useLayoutEffect(() => { // Runs before browser paints — safe to read DOM measurements here const height = elementRef.current.offsetHeight; setHeight(height); }, []); - useDebugValue shows hook values in React DevTools.
function useCustomHook(value) { useDebugValue(value > 10 ? "large" : "small"); return value; } - useTransition marks an update as non-urgent to keep UI responsive.
const [isPending, startTransition] = useTransition(); startTransition(() => { setSearchResults(results); // won't block input from being typed }); - useDeferredValue defers a value update without a transition.
const deferredQuery = useDeferredValue(searchQuery); const results = useMemo(() => filterList(deferredQuery), [deferredQuery]); - useId generates a stable unique ID for accessibility attributes.
function Input({ label }) { const id = useId(); return ( <> <label htmlFor={id}>{label}</label> <input id={id} /> </> ); }
React Hooks
(continued)Hook Rules
- Only call hooks at the top level, never in loops or conditions.
// Good function Component() { const [count, setCount] = useState(0); useEffect(() => { /* ... */ }, []); } // Bad: hook inside condition if (condition) { const [count, setCount] = useState(0); // Wrong! } - Only call hooks from React components or custom hooks.
- Use ESLint plugin to enforce rules automatically.
{ "plugins": ["react-hooks"], "rules": { "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn" } } - Custom hooks must start with "use" so React enforces the rules.
// Good: React treats useWindowWidth as a hook function useWindowWidth() { const [width, setWidth] = useState(window.innerWidth); useEffect(() => { const handler = () => setWidth(window.innerWidth); window.addEventListener("resize", handler); return () => window.removeEventListener("resize", handler); }, []); return width; } - Hooks must be called in the same order every render.
// React tracks hooks by call order — conditional calls break this // Move conditions inside the hook body, not around it useEffect(() => { if (!isLoggedIn) return; // condition inside — safe }, [isLoggedIn]);
React Hooks
(FAQ)FAQ
Pass a dependency array as the second argument to useEffect — an empty array [] runs the effect once on mount, while listing specific values like [userId] re-runs only when those values change. Avoid placing the fetch URL or options object directly in the dependency array if they're recreated each render, as object identity changes will trigger the effect repeatedly.
useState triggers a re-render when updated, while useRef stores a mutable value in .current that persists across renders without causing re-renders. Use useRef for values you need to track (like timers, previous values, or DOM nodes) but don't want to drive the UI.
Use them only after profiling confirms a real performance problem — wrapping every function or computed value adds overhead and complexity that often outweighs any benefit. The most justified cases are passing stable callbacks to heavily optimized child components (React.memo) or skipping expensive recalculations in tight render loops.
React 18 Strict Mode intentionally double-invokes effects in development to surface bugs in effects that don't properly clean up. Return a cleanup function from your effect to handle unmounting correctly, and your production build will only run the effect once.
Extract the state and effects into a custom hook — a plain JavaScript function whose name starts with 'use' that calls other hooks internally. Each component that calls the custom hook gets its own isolated state, so sharing logic doesn't mean sharing state.
React Lifecycle
Understand React component lifecycle phases and map class methods to hooks.
TL;DR
- 01Every component goes through mount, update, and unmount phases.
- 02Class lifecycle methods map onto specific hooks and timing rules.
- 03React skips re-renders when memoization or keys say state is unchanged.
Tips
- 01Map each class lifecycle method to its hooks equivalent one at a time when migrating, rather than rewriting a whole component at once.
- 02Use React.memo or PureComponent to skip unnecessary re-renders instead of fighting them with manual shouldComponentUpdate logic.
Warnings
- 01Calling setState synchronously inside componentDidMount or its hook equivalent forces React to render twice before the browser paints anything.
- 02Updating state after a component unmounts logs a warning in class components and can cause stale closures in hooks-based equivalents.
React Lifecycle
(continued)Component Lifecycle Phases
- Mount: component is created and inserted into the DOM.
- Update: component re-renders due to state or prop changes.
- Unmount: component is removed from the DOM.
function Component() { // Mount: run once useEffect(() => { console.log("Mounted"); }, []); // Unmount: cleanup useEffect(() => { return () => console.log("Unmounting"); }, []); } - React 18 Strict Mode mounts, unmounts, then remounts in development.
// In development with Strict Mode, lifecycle methods run twice // This is intentional — it proves teardown and setup are symmetric - The same component instance updates without dismounting on re-renders.
// Changing props or state updates the component // — it does NOT unmount and remount - A changed
keyprop forces React to unmount the old instance and mount a fresh one.// Changing the key remounts the component from scratch, // resetting all of its internal state <UserPanel key={userId} />
React Lifecycle
(continued)Class Lifecycle Methods
- componentDidMount fires once after the component is first inserted into the DOM.
class Profile extends React.Component { componentDidMount() { console.log("Mounted, safe to fetch or measure DOM"); } } - componentDidUpdate fires after every re-render except the first, receiving previous props and state.
componentDidUpdate(prevProps, prevState) { if (prevProps.userId !== this.props.userId) { this.loadUser(this.props.userId); } } - componentWillUnmount fires once, right before React removes the component from the DOM.
componentWillUnmount() { this.subscription.unsubscribe(); } - shouldComponentUpdate runs before re-rendering and can return false to skip the render entirely.
shouldComponentUpdate(nextProps) { // Skip re-render if the displayed value hasn't changed return nextProps.value !== this.props.value; } - getDerivedStateFromProps runs before every render to sync state from incoming props.
static getDerivedStateFromProps(props, state) { if (props.id !== state.prevId) { return { selected: null, prevId: props.id }; } return null; }
React Lifecycle
(continued)Class to Hooks Mapping
- componentDidMount maps to useEffect with an empty dependency array, since both run once after the initial render.
// Class: componentDidMount() { ... } useEffect(() => { // runs once, after mount }, []); - componentDidUpdate maps to useEffect with specific dependencies, since both run after updates to those values.
// Class: componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) ... } useEffect(() => { // runs after mount AND after every id change }, [id]); - componentWillUnmount maps to the function returned from useEffect.
// Class: componentWillUnmount() { this.sub.unsubscribe(); } useEffect(() => { const sub = subscribe(); return () => sub.unsubscribe(); }, []); - shouldComponentUpdate maps to wrapping the function component in React.memo.
// Class: extends React.PureComponent, or custom shouldComponentUpdate const Row = React.memo((props) => { return <tr>{props.label}</tr>; }); - getDerivedStateFromProps usually maps to deriving the value directly during render instead of storing it in state.
// Class: static getDerivedStateFromProps synced state.prevId to props.id const [prevId, setPrevId] = useState(id); if (id !== prevId) { setPrevId(id); setSelected(null); // adjust state during render, not in an effect }
React Lifecycle
(continued)Reconciliation and Re-Renders
- A parent re-rendering re-renders every child by default, even children whose props are unchanged.
// Parent state change re-renders Child even if childProp never changes function Parent() { const [count, setCount] = useState(0); return <Child childProp="static" />; } - React.memo skips re-rendering a function component when its props are shallowly equal to the last render.
const Child = React.memo(({ childProp }) => { return <div>{childProp}</div>; }); // re-renders only when childProp's reference changes - PureComponent is the class equivalent of React.memo, shallow-comparing props and state automatically.
class Row extends React.PureComponent { // Automatically skips render if props/state are shallowly equal render() { return <tr>{this.props.label}</tr>; } } - Changing a list item's key forces React to discard the old DOM node and lifecycle state instead of updating it in place.
// Using array index as key can cause React to reuse the wrong instance // when items are reordered — prefer a stable id items.map((item) => <Row key={item.id} item={item} />) - useMemo and useCallback stop new object or function references from defeating memoization on the next render.
// Without useCallback, onSave is a new function every render, // which breaks React.memo on Child const onSave = useCallback(() => save(id), [id]);
React Lifecycle
(continued)Common Lifecycle Bugs
- Calling setState synchronously inside componentDidMount triggers an extra render before the browser paints.
componentDidMount() { // Causes a second render right after the first — avoid when possible this.setState({ ready: true }); } - Updating state in componentWillUnmount or after an effect's cleanup has run logs a no-op warning.
componentWillUnmount() { // Warning: Can't perform a React state update on an unmounted component this.setState({ closed: true }); // remove this call instead } - Child lifecycle methods fire before the parent's during mount, but the parent's fire first during unmount.
// Mount order: Child.componentDidMount, then Parent.componentDidMount // Unmount order: Parent.componentWillUnmount, then Child.componentWillUnmount - Calling shouldComponentUpdate but forgetting to compare nested fields silently blocks needed re-renders.
shouldComponentUpdate(nextProps) { // Bug: compares the array reference, not its contents return nextProps.items !== this.props.items; } - Mixing getDerivedStateFromProps with side effects like fetching breaks its contract — it must stay pure.
static getDerivedStateFromProps(props) { // Wrong: side effects don't belong here, this method must be pure // fetch(props.url) — move data fetching to componentDidMount/useEffect instead return null; }
React Lifecycle
(FAQ)FAQ
componentDidMount is a class lifecycle method that fires once after the initial render, before the browser paints in some cases. useEffect with an empty dependency array is the closest functional equivalent, but it always runs after paint, and it folds in cleanup logic that previously required a separate componentWillUnmount method.
React re-renders a component whenever its parent re-renders, regardless of whether the props look the same, unless you opt out with React.memo or shouldComponentUpdate. Object and array props that are recreated on every parent render also count as changed, because React compares references, not deep equality.
React calls componentWillUnmount (or runs effect cleanup functions) synchronously before detaching the component from the DOM. This is the only guaranteed point to cancel timers, close connections, and unsubscribe — after this, the component instance is discarded and any further state updates are ignored or warned about.
Yes — wrap function components in React.memo or extend PureComponent for class components, both of which shallow-compare props and skip rendering when nothing changed. For state-driven re-renders inside the same component, shouldComponentUpdate or splitting state into smaller, more targeted pieces avoids unnecessary work.
React 18 Strict Mode intentionally mounts, unmounts, and remounts components in development to surface lifecycle code that isn't resilient to being torn down and set up again. This double-invoke only happens in development with Strict Mode enabled — production builds run each lifecycle step once.
React Portals
Use portals to render components outside the DOM hierarchy for modals and overlays.
TL;DR
- 01Use ReactDOM.createPortal to render outside the DOM tree.
- 02Portals are useful for modals, tooltips, and dropdowns.
- 03Event bubbling still works through portals to parent components.
Tips
- 01Use portals for any UI that needs to visually escape its parent container without needing absolute positioning tricks.
Warnings
- 01Event bubbling in portals goes through the React tree, not the DOM tree, so document event listeners won't catch portal events from within React.
React Portals
(continued)Creating Portals
- Use ReactDOM.createPortal to render a component at a different location.
import { createPortal } from "react-dom"; function Modal({ children }) { const root = document.getElementById("modal-root"); return createPortal( <div className="modal">{children}</div>, root ); } - Create a target element in your HTML for the portal.
<div id="root"></div> <div id="modal-root"></div> - Portals render outside the component hierarchy but stay in React.
- Perfect for modals, dropdowns, tooltips, and overlays.
- Check that the target element exists before rendering.
function Portal({ children, containerId = "portal-root" }) { const container = document.getElementById(containerId); if (!container) return null; return createPortal(children, container); }
React Portals
(continued)Modal Implementation
- Build a reusable modal using portals for proper layering.
function Modal({ isOpen, onClose, children, title }) { if (!isOpen) return null; return createPortal( <div className="modal-backdrop" onClick={onClose}> <div className="modal-content" onClick={e => e.stopPropagation()}> <h2>{title}</h2> {children} <button onClick={onClose}>Close</button> </div> </div>, document.getElementById("portal-root") ); } function App() { const [open, setOpen] = useState(false); return ( <> <button onClick={() => setOpen(true)}>Open Modal</button> <Modal isOpen={open} onClose={() => setOpen(false)} title="Hello"> Modal content here </Modal> </> ); } - Use stopPropagation to prevent backdrop clicks from closing the modal.
- Trap keyboard focus inside the modal for accessibility compliance.
useEffect(() => { if (!isOpen) return; const focusable = modalRef.current?.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); focusable?.[0]?.focus(); }, [isOpen]); - Close the modal when the user presses the Escape key.
useEffect(() => { function handleKey(e) { if (e.key === "Escape") onClose(); } if (isOpen) document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [isOpen, onClose]); - Prevent body scroll while the modal is open.
useEffect(() => { document.body.style.overflow = isOpen ? "hidden" : ""; return () => { document.body.style.overflow = ""; }; }, [isOpen]);
React Portals
(continued)Event Bubbling Through Portals
- Events bubble from portals to ancestors in the React component tree.
function Parent() { const handleClick = (e) => { if (e.target.closest(".modal")) { console.log("Modal clicked"); } }; return ( <div onClick={handleClick}> <Modal>Content</Modal> </div> ); } - Event bubbling goes through the React tree, not the DOM tree.
- This allows parent components to handle events from portaled children.
- Useful for closing modals when clicking outside.
- Use stopPropagation inside the portal to prevent bubbling to parent handlers.
function Modal({ onClose, children }) { return createPortal( <div className="backdrop" onClick={onClose}> <div className="content" onClick={e => e.stopPropagation()}> {children} </div> </div>, document.getElementById("modal-root") ); }
React Portals
(continued)Common Portal Use Cases
- Tooltips that overflow their containers and need body-level placement.
function Tooltip({ content, children }) { return ( <> {children} {createPortal( <div className="tooltip">{content}</div>, document.body )} </> ); } - Dropdowns and autocomplete menus that escape overflow-hidden parents.
function Dropdown({ isOpen, options }) { if (!isOpen) return null; return createPortal( <ul className="dropdown"> {options.map(opt => <li key={opt}>{opt}</li>)} </ul>, document.body ); } - Notifications and toasts floating above all other content.
function Toast({ message }) { return createPortal( <div className="toast">{message}</div>, document.getElementById("toast-container") ); } - Context menus that must appear at the cursor position on the page.
function ContextMenu({ x, y, items, onClose }) { return createPortal( <ul className="context-menu" style={{ top: y, left: x }}> {items.map(item => ( <li key={item.label} onClick={() => { item.action(); onClose(); }}> {item.label} </li> ))} </ul>, document.body ); } - Lightboxes that render full-screen images above all page content.
function Lightbox({ src, alt, onClose }) { return createPortal( <div className="lightbox" onClick={onClose}> <img src={src} alt={alt} /> </div>, document.getElementById("portal-root") ); }
React Portals
(continued)Portal Best Practices
- Always have a target element in your HTML.
<body> <div id="root"></div> <div id="modal-root"></div> <div id="tooltip-root"></div> </body> - Clean up portals when components unmount.
useEffect(() => { return () => { // Cleanup if needed }; }, []); - Use z-index in CSS to layer portaled elements correctly.
.modal-backdrop { z-index: 1000; } - Manage focus when opening a modal for keyboard accessibility.
useEffect(() => { if (isOpen) { closeButtonRef.current?.focus(); } }, [isOpen]); - Add role="dialog" and aria-modal="true" for screen reader support.
createPortal( <div role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 id="modal-title">Confirm Delete</h2> {children} </div>, document.getElementById("modal-root") )
React Portals
(FAQ)FAQ
Use ReactDOM.createPortal(element, document.body) to mount the modal directly on the body while keeping it inside your React tree. This means context providers and state from parent components remain accessible inside the portal.
Portal events bubble through the React component tree, not the DOM tree, so native document-level listeners won't fire for clicks inside a portal. Move event handling into React components using onClick or similar synthetic event handlers instead.
Portals are preferable when a parent component has overflow: hidden, transform, or z-index stacking context issues that would clip or obscure your overlay even with fixed positioning. Use position: fixed inside a portal for the cleanest combination of both.
Yes — portal children still belong to the React tree where createPortal is called, so Context, Redux state, and hooks all work normally even though the portal renders to a different DOM node.
createPortal keeps the component inside React's reconciliation cycle, giving you lifecycle methods, hooks, and automatic cleanup on unmount. Raw DOM manipulation bypasses React entirely, requiring manual cleanup and losing all React features.
React Ref Patterns
Real-world React ref patterns: click-outside detection, ref arrays, previous value tracking, ResizeObserver, and TypeScript typing.
TL;DR
- 01Use ref.current.contains() to detect clicks outside a component.
- 02Track a previous value by assigning to a ref inside useEffect.
- 03Combine useRef with ResizeObserver for accurate element measurements.
Tips
- 01Wrap the callback prop in useCallback in the parent to keep the dependency array stable and avoid re-registering the document listener on every render.
Warnings
- 01On the very first render, usePrevious returns undefined because no previous value exists. Guard with prevValue !== undefined before using it in comparisons.
React Ref Patterns
(continued)Click-Outside Detection
The click-outside pattern is essential for dropdowns, modals, and tooltips. Attach a ref to the container, listen for mousedown on document, and call contains() to decide whether to close.
- Full reusable hook:
import { useRef, useEffect } from 'react'; function useClickOutside(callback) { const ref = useRef(null); useEffect(() => { function handleMouseDown(e) { if (ref.current && !ref.current.contains(e.target)) { callback(); } } document.addEventListener('mousedown', handleMouseDown); return () => document.removeEventListener('mousedown', handleMouseDown); }, [callback]); return ref; } function Dropdown() { const [open, setOpen] = useState(false); const dropdownRef = useClickOutside(() => setOpen(false)); return ( <div ref={dropdownRef}> <button onClick={() => setOpen(o => !o)}>Toggle</button> {open && <ul><li>Item 1</li><li>Item 2</li></ul>} </div> ); } - Use
mousedownrather thanclickso the close action fires before any click handler on the newly focused element. This prevents flickering when clicking between two dropdowns. - Guard with
ref.current &&before callingcontains— the ref may be null if the component unmounted between renders.
React Ref Patterns
(continued)Ref Arrays for Dynamic Lists
When you render a list with map and need a ref to each item, you can't call useRef in a loop (that violates the Rules of Hooks). Instead, store an array inside a single ref and populate it with callback refs.
- Hold all item refs in one ref object and assign each slot via a callback ref:
import { useRef } from 'react'; function VirtualList({ items }) { const itemRefs = useRef([]); function scrollToItem(index) { itemRefs.current[index]?.scrollIntoView({ behavior: 'smooth' }); } return ( <> <button onClick={() => scrollToItem(5)}>Jump to #5</button> <ul> {items.map((item, i) => ( <li key={item.id} ref={node => { itemRefs.current[i] = node; }} > {item.label} </li> ))} </ul> </> ); } - The inline callback
node => { itemRefs.current[i] = node; }runs each render, keeping the array in sync when items are added or removed. When a node unmounts React calls the callback withnull, so you may want to filter:itemRefs.current = itemRefs.current.filter(Boolean)before use. - This pattern also works with
Mapkeyed by item ID for sparse or keyed access:const refMap = useRef(new Map()); // ... ref={node => { if (node) refMap.current.set(item.id, node); else refMap.current.delete(item.id); }}
React Ref Patterns
(continued)Previous Value Tracking
React has no built-in way to read a previous prop or state value, but a ref updated inside useEffect gives you exactly that — the value from the render that just committed.
- Generic
usePrevioushook:import { useRef, useEffect } from 'react'; function usePrevious(value) { const ref = useRef(undefined); useEffect(() => { ref.current = value; // runs AFTER render, so ref holds previous value during render }, [value]); return ref.current; } function PriceDisplay({ price }) { const prevPrice = usePrevious(price); const direction = price > prevPrice ? '↑' : price < prevPrice ? '↓' : ''; return ( <p> {price} {direction} (was {prevPrice ?? 'N/A'}) </p> ); } - How the timing works: during render,
ref.currentstill holds the previous value because the effect hasn't fired yet. After React commits the DOM, the effect updatesref.currentto the new value, ready for the next render. - Use this to skip an effect on the initial render by comparing current and previous values, or to animate differences between frames.
React Ref Patterns
(continued)Measuring with ResizeObserver
ResizeObserver fires whenever a watched element's content box changes size. Combining it with useRef gives you live width and height measurements without polling or window resize events.
- Full hook that returns live dimensions:
import { useRef, useState, useEffect } from 'react'; function useElementSize() { const ref = useRef(null); const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const node = ref.current; if (!node) return; const observer = new ResizeObserver(([entry]) => { const { width, height } = entry.contentRect; setSize({ width: Math.round(width), height: Math.round(height) }); }); observer.observe(node); return () => observer.disconnect(); // always clean up }, []); return [ref, size]; } function ResponsiveCard() { const [cardRef, { width, height }] = useElementSize(); return ( <div ref={cardRef} style={{ resize: 'both', overflow: 'auto', padding: 16 }}> <p>{width} × {height}px — drag the corner to resize</p> </div> ); } - ResizeObserver is available in all modern browsers (Chrome 64+, Firefox 69+, Safari 13.1+). For older environments, polyfill with
resize-observer-polyfill. - The observer fires once immediately after
observe()is called, sosizeis populated on mount without an extra effect or state initialization.
React Ref Patterns
(continued)TypeScript: Typing Refs
TypeScript has two ref types. Choosing the right one avoids unsafe non-null assertions and keeps autocompletion accurate.
| Type | current | Created by | Use case |
|---|---|---|---|
RefObject<T> | T | null (readonly) | useRef<T>(null) | DOM nodes React controls |
MutableRefObject<T> | T (writable) | useRef<T>(initialValue) | Timer IDs, counters, previous values |
- DOM element ref — always start with
null:const inputRef = useRef<HTMLInputElement>(null); // inputRef is RefObject<HTMLInputElement> // Safe access with optional chaining: inputRef.current?.focus(); - Mutable value ref — supply a real initial value so TypeScript infers
MutableRefObject:const timerId = useRef<ReturnType<typeof setTimeout> | null>(null); // Assign later: timerId.current = setTimeout(fn, 500); clearTimeout(timerId.current!); - Typing a component that accepts a ref (React 19 style):
import { useRef, useImperativeHandle } from 'react'; type InputHandle = { focus: () => void; clear: () => void; }; type InputProps = React.InputHTMLAttributes<HTMLInputElement> & { ref?: React.Ref<InputHandle>; }; function FancyInput({ ref, ...props }: InputProps) { const innerRef = useRef<HTMLInputElement>(null); useImperativeHandle(ref, () => ({ focus: () => innerRef.current?.focus(), clear: () => { if (innerRef.current) innerRef.current.value = ''; }, })); return <input ref={innerRef} {...props} />; } // Parent: const formRef = useRef<InputHandle>(null); <FancyInput ref={formRef} placeholder="Email" /> - Avoid using
as React.MutableRefObject<T>to force-cast DOM refs — this bypasses null safety. Instead, use optional chaining or an early-return null check.
React Ref Patterns
(FAQ)FAQ
Attach a ref to the dropdown container, then add a mousedown listener on document inside a useEffect. In the handler, call ref.current.contains(e.target) — if it returns false, the click was outside and you can close the dropdown. Remove the listener in the cleanup function returned from useEffect.
Initialise a ref with useRef([]) to hold the array of DOM nodes, then pass each item a callback ref function that assigns node into the array at the correct index: ref={node => { listRef.current[i] = node; }}. This approach works even when the list length changes between renders.
Create a ref with useRef(), then run useEffect(() => { prevRef.current = value; }, [value]) after each render. Because effects run after the render is committed, prevRef.current always holds the value from the previous render cycle when you read it during the current render.
Use ResizeObserver when you need continuous size updates as the element grows or shrinks — for example, when the user resizes the window or when dynamic content changes the element's dimensions. getBoundingClientRect is a one-shot read and won't notify you of future changes. ResizeObserver fires a callback each time the observed element's size changes.
RefObject has a readonly current property typed as T | null — it models a DOM ref that React controls. MutableRefObject has a mutable current typed as T — it models a ref you update yourself, like a timer ID or previous value store. useRef(null) returns RefObject
React State Management
Manage application state with useState, useReducer, Context, Redux, or Zustand.
TL;DR
- 01Colocate state as close as possible to where it is used to avoid unnecessary re-renders.
- 02Use React Query or SWR for server state; reserve useState and Zustand for client-only state.
- 03Fix Redux performance with configureStore from Redux Toolkit — createStore is deprecated.
Tips
- 01Most apps have far less global state than developers think — UI state like hover, focus, and toggles almost always belongs locally, not in a shared store.
- 02Use typed hooks (useAppSelector, useAppDispatch) rather than the raw hooks so you get full TypeScript inference without casting at every call site.
- 03Start with useState and useContext, only add Redux or Zustand when state becomes too complex to manage.
Warnings
- 01Don't over-engineer state management — use the simplest solution that works for your app size.
React State Management
(continued)Server State vs Client State
Server state is data that lives on the server and needs to stay in sync — fetch it with React Query or SWR, not useState.
import { useQuery } from "@tanstack/react-query"; function UserProfile({ id }: { id: string }) { const { data, isLoading, error } = useQuery({ queryKey: ["user", id], queryFn: () => fetch(`/api/users/${id}`).then(r => r.json()), staleTime: 60_000 // cache for 1 minute }); if (isLoading) return <p>Loading...</p>; if (error) return <p>Error loading user.</p>; return <h1>{data.name}</h1>; }Client state is UI-only data that doesn't need to persist on the server — modal open/closed, selected tab, input value.
// Client state: lives in the component, no server sync needed const [isMenuOpen, setIsMenuOpen] = useState(false); const [activeTab, setActiveTab] = useState("overview");Use SWR for lightweight data fetching with automatic revalidation on focus and reconnect.
import useSWR from "swr"; const fetcher = (url: string) => fetch(url).then(r => r.json()); function Dashboard() { const { data } = useSWR("/api/stats", fetcher, { refreshInterval: 30000 }); return <p>Users: {data?.count}</p>; }Avoid duplicating server state in useState — let the fetching library own it and read from its cache.
// Wrong: duplicating server data into local state const [user, setUser] = useState(null); useEffect(() => { fetch("/api/me").then(r => r.json()).then(setUser); }, []); // Right: let React Query own the state const { data: user } = useQuery({ queryKey: ["me"], queryFn: () => fetch("/api/me").then(r => r.json()) });Invalidate cache entries after mutations so the UI stays in sync without manual state updates.
const queryClient = useQueryClient(); await updateUser(data); queryClient.invalidateQueries({ queryKey: ["user", id] }); // refetches automatically
React State Management
(continued)State Colocation
Place state as close as possible to where it is used — don't lift state higher than necessary.
// Wrong: lifting filter state to the top-level App when only ProductList needs it function App() { const [filter, setFilter] = useState("all"); return <ProductList filter={filter} onFilterChange={setFilter} />; } // Right: colocate filter state inside ProductList where it belongs function ProductList() { const [filter, setFilter] = useState("all"); // ...no prop drilling needed }State used only in one component belongs in that component, not in a parent or global store.
State used by two sibling components should live in their closest common ancestor (lift state up one level, not all the way up).
// Both SearchBox and SearchResults need the query — lift to their parent function SearchPage() { const [query, setQuery] = useState(""); return ( <> <SearchBox value={query} onChange={setQuery} /> <SearchResults query={query} /> </> ); }State used across unrelated parts of the app belongs in a global store (Zustand, Context, or Redux).
Avoid premature globalization — start local and lift only when a second consumer appears.
// Progression: local → lifted → global // 1. useState in component (local) // 2. useState in parent (lifted) — when a sibling needs it // 3. Zustand or Context (global) — when distant components need it
React State Management
(continued)State Performance Patterns
Use functional state updates when new state depends on previous state to avoid stale closures.
// Wrong: may read stale count if React batches updates setCount(count + 1); // Right: always uses the latest committed state setCount(prev => prev + 1); setItems(prev => [...prev, newItem]);Batch related state updates with
Object.assignor a single state object to avoid extra renders.// Two setState calls = two renders setLoading(true); setError(null); // One setState call = one render setState(prev => ({ ...prev, loading: true, error: null }));Use Zustand's selector to subscribe only to the slice of state a component needs.
const useStore = create((set) => ({ count: 0, user: null, theme: "light", setCount: (n) => set({ count: n }), })); // Only re-renders when count changes, not when user or theme changes const count = useStore(state => state.count);Derive values during render instead of storing derived state — computed values don't need setState.
// Wrong: storing derived state const [items, setItems] = useState([]); const [count, setCount] = useState(0); // always equals items.length // Right: derive during render const [items, setItems] = useState([]); const count = items.length; // computed, always accurate, no sync neededUse
React.memoplus stable callback references to prevent child re-renders from parent state changes.const handleClick = useCallback(() => dispatch({ type: "SUBMIT" }), [dispatch]); const ExpensiveChild = React.memo(({ onClick }) => <button onClick={onClick}>Submit</button>);
React State Management
(continued)Redux for Large Apps
Use Redux Toolkit's
configureStore—createStoreis deprecated since Redux 4.x.import { configureStore } from "@reduxjs/toolkit"; import counterReducer from "./counterSlice"; export const store = configureStore({ reducer: { counter: counterReducer } }); export type RootState = ReturnType<typeof store.getState>; export type AppDispatch = typeof store.dispatch;Define slices with
createSliceto eliminate action type constants and manual spread reducers.import { createSlice } from "@reduxjs/toolkit"; const counterSlice = createSlice({ name: "counter", initialState: { value: 0 }, reducers: { increment: state => { state.value += 1; }, // Immer allows direct mutation decrement: state => { state.value -= 1; }, setValue: (state, action) => { state.value = action.payload; } } }); export const { increment, decrement, setValue } = counterSlice.actions; export default counterSlice.reducer;Wrap your app in
<Provider store={store}>to make state available to all components.import { Provider } from "react-redux"; root.render(<Provider store={store}><App /></Provider>);Read state with
useSelectorand dispatch actions withuseDispatch.function Counter() { const count = useSelector((state: RootState) => state.counter.value); const dispatch = useDispatch<AppDispatch>(); return <button onClick={() => dispatch(increment())}>{count}</button>; }Use Redux DevTools to inspect state history, replay actions, and debug time-travel in development.
React State Management
(continued)Lightweight Alternatives
Create a Zustand store with state and actions in a single
createcall.import { create } from "zustand"; const useStore = create<{ count: number; increment: () => void }>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), })); function Counter() { const { count, increment } = useStore(); return <button onClick={increment}>Count: {count}</button>; }Use Jotai atoms for fine-grained, component-level shared state without a central store.
import { atom, useAtom } from "jotai"; const countAtom = atom(0); function Counter() { const [count, setCount] = useAtom(countAtom); return <button onClick={() => setCount((c) => c + 1)}>{count}</button>; }Use Recoil atoms and selectors to share state and compute derived values declaratively.
import { atom, selector } from "recoil"; const textState = atom({ key: "textState", default: "" }); const charCountState = selector({ key: "charCountState", get: ({ get }) => get(textState).length, // derived from textState });Access Zustand state anywhere without wrapping the app in a Provider.
// No <Provider> needed — call the hook directly in any component function Navbar() { const user = useStore((state) => state.user); // selector for one slice return <p>Hello {user?.name}</p>; }Choose Zustand or Jotai over Redux when you want minimal boilerplate for small to medium apps.
// Redux: actions + reducer + selectors + Provider setup // Zustand: one create() call, one hook, no Provider required // Jotai: one atom() call, one useAtom() hook, no Provider required const theme = useStore((s) => s.theme); // direct selector access
React State Management
(FAQ)FAQ
Use useReducer when you have multiple related state values that change together, or when next state depends on the previous state in complex ways. If you find yourself writing several useState calls that always update together, that's a signal to consolidate with useReducer.
Context API replaces Redux for many medium-sized apps, but it's not optimized for high-frequency updates — every consumer re-renders when context value changes. For apps with frequent global state updates or large teams needing strict state patterns, Redux or Zustand will perform better.
Zustand has a much smaller API surface and requires no boilerplate — you define a store in a single function call versus Redux's actions, reducers, and selectors. Choose Zustand for smaller teams or projects where Redux's structure feels like overhead.
Any state change in the context value causes all consumers to re-render, even if they only use a part of the value. Fix this by splitting large contexts into smaller focused ones, or memoizing the context value with useMemo.
Yes, and it's often the right call — local UI state in useState, shared server data in React Query or SWR, and global client state in Context or Zustand can coexist cleanly. Mixing solutions by concern keeps each layer simple rather than forcing everything through one system.
React Suspense
Load components and data with Suspense boundaries for better UX and streaming.
TL;DR
- 01Use Suspense to show loading states while components or data load.
- 02Wrap lazy-loaded components in Suspense for fallback UI.
- 03Combine with error boundaries for complete async error handling.
Tips
- 01Nest Suspense boundaries at different levels to show partial content incrementally while the rest loads.
Warnings
- 01Suspense for data fetching is still experimental in React 19 — check version compatibility and use established libraries like React Query for production apps.
React Suspense
(continued)Suspense Basics
- Wrap Suspense around components that may suspend.
import { Suspense, lazy } from "react"; const HeavyComponent = lazy(() => import("./HeavyComponent")); export default function App() { return ( <Suspense fallback={<p>Loading...</p>}> <HeavyComponent /> </Suspense> ); } - The fallback prop shows while the component is loading.
- Once loaded, the component replaces the fallback.
- Suspense can wrap multiple components at once.
<Suspense fallback={<div>Loading page...</div>}> <Header /> <MainContent /> <Sidebar /> </Suspense> - Use a skeleton or spinner as the fallback for a better UX.
<Suspense fallback={<PageSkeleton />}> <Dashboard /> </Suspense>
React Suspense
(continued)Code Splitting with Suspense
- Use lazy to split code and load components on demand.
const Dashboard = lazy(() => import("./pages/Dashboard")); const Settings = lazy(() => import("./pages/Settings")); export default function App({ page }) { return ( <Suspense fallback={<p>Loading page...</p>}> {page === "dashboard" && <Dashboard />} {page === "settings" && <Settings />} </Suspense> ); } - Each lazy component is a separate code chunk.
- Chunks load only when the component is about to render.
- Significantly reduces initial bundle size for large apps.
- Combine with React Router for route-based code splitting.
const Home = lazy(() => import("./pages/Home")); const Profile = lazy(() => import("./pages/Profile")); <Suspense fallback={<p>Loading...</p>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/profile" element={<Profile />} /> </Routes> </Suspense>
React Suspense
(continued)Data Fetching with Suspense
- Use React Query or SWR with Suspense for data fetching.
function UserProfile({ userId }) { const { data: user } = useSuspenseQuery({ queryKey: ["user", userId], queryFn: () => fetchUser(userId) }); return <div>{user.name}</div>; } <Suspense fallback={<p>Loading user...</p>}> <UserProfile userId={1} /> </Suspense> - Suspense handles loading states automatically.
- No need for manual loading state management.
- Error boundaries catch fetch errors thrown outside Suspense.
- Wrap the data-fetching component, not the calling component.
// UserProfile suspends internally — wrap it here function Page() { return ( <Suspense fallback={<Spinner />}> <UserProfile userId={userId} /> </Suspense> ); }
React Suspense
(continued)Nested Suspense Boundaries
- Use multiple Suspense boundaries for granular control.
<Suspense fallback={<p>Loading page...</p>}> <Header /> <Suspense fallback={<p>Loading content...</p>}> <MainContent /> </Suspense> <Suspense fallback={<p>Loading sidebar...</p>}> <Sidebar /> </Suspense> </Suspense> - Each boundary can have its own fallback UI.
- Inner boundaries resolve independently of each other.
- Outer fallback shows only for outer-level suspensions.
- Great for showing partial content while loading.
- Place boundaries close to each suspending component for best UX.
// Narrow boundary: only hides the part that's loading function ProductList() { return ( <ul> {productIds.map(id => ( <Suspense key={id} fallback={<li>Loading...</li>}> <ProductItem id={id} /> </Suspense> ))} </ul> ); }
React Suspense
(continued)Advanced Patterns
- Combine Suspense with Error Boundaries.
<ErrorBoundary fallback={<p>Error loading page</p>}> <Suspense fallback={<p>Loading...</p>}> <PageContent /> </Suspense> </ErrorBoundary> - Transition to new content with useTransition.
function App() { const [isPending, startTransition] = useTransition(); const [page, setPage] = useState("home"); const navigate = (newPage) => { startTransition(() => setPage(newPage)); }; return ( <Suspense fallback={<p>Loading...</p>}> {isPending && <p>Loading new page...</p>} <PageContent page={page} /> </Suspense> ); } - Use startTransition to keep the old UI visible while new content loads.
- Preload lazy components early to avoid loading delays.
// Preload on hover before user clicks const LazyPage = lazy(() => import("./Page")); function NavLink({ href, children }) { return ( <a href={href} onMouseEnter={() => import("./Page")}> {children} </a> ); } - Use the react-error-boundary package for ready-made Error Boundaries.
import { ErrorBoundary } from "react-error-boundary"; <ErrorBoundary fallbackRender={({ error }) => <p>Error: {error.message}</p>}> <Suspense fallback={<Spinner />}> <AsyncComponent /> </Suspense> </ErrorBoundary>
React Suspense
(FAQ)FAQ
Suspense for data fetching is experimental in React 19 and not recommended for production without a supporting library. Use React Query or SWR instead — they integrate with Suspense via their own stable APIs and handle caching, retries, and deduplication.
Wrap React.lazy(() => import('./MyComponent')) in a Suspense boundary with a fallback prop: <Suspense fallback={
All siblings suspend together — the single fallback shows until every component in the boundary is ready. To show components progressively as they load, wrap each in its own Suspense boundary so resolved components render immediately without waiting for slower siblings.
Suspense handles the loading state but not errors — a thrown promise triggers the fallback, while a thrown error propagates up to the nearest error boundary. Always pair Suspense with an ErrorBoundary component wrapping it to catch fetch failures or import errors.
Yes — Next.js App Router uses React's streaming SSR with Suspense, sending HTML in chunks as each boundary resolves on the server. Wrap async Server Components in Suspense to stream partial content to the client rather than blocking the entire page render.
React Testing
Test React components with Jest and React Testing Library to verify user behavior.
TL;DR
- 01Use React Testing Library to render components and query the DOM.
- 02Simulate user actions with userEvent, not the lower-level fireEvent.
- 03Query by role and visible text, not by class names or test IDs.
Tips
- 01Test user behavior, not implementation — query by role or visible text, not by test IDs.
Warnings
- 01Avoid testing implementation details like internal state — test what users see and interact with.
React Testing
(continued)Basic Test Setup
- Create a test file next to the component.
// Button.test.js import { render, screen } from '@testing-library/react'; import Button from './Button'; test('renders a button', () => { render(<Button label="Click me" />); expect(screen.getByRole('button')).toBeInTheDocument(); }); - Run tests with the npm test command.
npm test # watch mode npm test -- --ci # run once for CI npm test -- --coverage # generate coverage report - Install the necessary packages for a new project.
npm install --save-dev @testing-library/react @testing-library/user-event @testing-library/jest-dom - Import jest-dom matchers to enable toBeInTheDocument and similar.
// setupTests.js — imported in jest config import '@testing-library/jest-dom'; - Use describe to group related tests.
describe("Button", () => { test("renders the label", () => { /* ... */ }); test("calls onClick when pressed", () => { /* ... */ }); test("is disabled when prop is set", () => { /* ... */ }); });
React Testing
(continued)Rendering Components
- Render a component and query the result with screen.
import { render, screen } from '@testing-library/react'; test('renders component', () => { render(<MyComponent />); expect(screen.getByText('Expected text')).toBeInTheDocument(); }); - Use semantic queries to find elements the way users see them.
screen.getByRole('button', { name: 'Submit' }); screen.getByText('Label'); screen.getByPlaceholderText('Enter name'); screen.getByLabelText('Email'); - Use getBy for elements that must exist, queryBy for optional ones.
screen.getByRole('button'); // throws if missing screen.queryByText('Error'); // returns null if missing await screen.findByText('Loaded'); // waits for element to appear - Wrap providers around components that need context.
test('shows user name from context', () => { render( <UserContext.Provider value={{ user: { name: 'Alice' } }}> <Greeting /> </UserContext.Provider> ); expect(screen.getByText('Hello, Alice')).toBeInTheDocument(); }); - Use rerender to test how a component responds to prop changes.
const { rerender } = render(<Badge count={0} />); expect(screen.getByText('0')).toBeInTheDocument(); rerender(<Badge count={5} />); expect(screen.getByText('5')).toBeInTheDocument();
React Testing
(continued)User Interactions
- Simulate user clicks with userEvent.click.
import userEvent from '@testing-library/user-event'; test('handles click', async () => { const user = userEvent.setup(); render(<Counter />); const button = screen.getByRole('button', { name: 'Increment' }); await user.click(button); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); - Type into inputs with userEvent.type.
test('updates input on type', async () => { const user = userEvent.setup(); render(<SearchBox />); const input = screen.getByPlaceholderText('Search...'); await user.type(input, 'React hooks'); expect(input).toHaveValue('React hooks'); }); - Submit a form to test validation and submission handlers.
test('submits the form', async () => { const user = userEvent.setup(); const onSubmit = jest.fn(); render(<LoginForm onSubmit={onSubmit} />); await user.type(screen.getByLabelText('Email'), 'alice@example.com'); await user.type(screen.getByLabelText('Password'), 'secret123'); await user.click(screen.getByRole('button', { name: 'Log in' })); expect(onSubmit).toHaveBeenCalledWith({ email: 'alice@example.com' }); }); - Check a checkbox or select from a dropdown.
await user.click(screen.getByRole('checkbox', { name: 'Accept terms' })); expect(screen.getByRole('checkbox')).toBeChecked(); await user.selectOptions(screen.getByRole('combobox'), 'Option B'); - Use keyboard navigation to test accessibility interactions.
await user.keyboard('{Tab}'); // move focus await user.keyboard('{Enter}'); // activate focused element await user.keyboard('{Escape}'); // close modal
React Testing
(continued)Async Testing
- Wait for elements to appear with waitFor.
import { waitFor } from '@testing-library/react'; test('loads data', async () => { render(<DataComponent />); await waitFor(() => { expect(screen.getByText('Data loaded')).toBeInTheDocument(); }); }); - Use findBy queries as a shorter alternative to waitFor.
// findBy automatically waits — no waitFor needed const item = await screen.findByText('Loaded item'); expect(item).toBeInTheDocument(); - Mock fetch to control async responses in tests.
global.fetch = jest.fn(() => Promise.resolve({ json: () => Promise.resolve([{ id: 1, name: "Alice" }]) }) ); test('renders fetched data', async () => { render(<UserList />); expect(await screen.findByText('Alice')).toBeInTheDocument(); }); - Test loading and error states for async components.
test('shows loading then data', async () => { render(<DataComponent />); expect(screen.getByText('Loading...')).toBeInTheDocument(); expect(await screen.findByText('Alice')).toBeInTheDocument(); }); - Use MSW (Mock Service Worker) for realistic API mocking.
import { http, HttpResponse } from 'msw'; import { server } from './mocks/server'; test('handles API error', async () => { server.use(http.get('/api/users', () => HttpResponse.error())); render(<UserList />); expect(await screen.findByText('Failed to load')).toBeInTheDocument(); });
React Testing
(continued)Mocking
- Mock a function with jest.fn to track calls and set return values.
const mockOnClick = jest.fn(); render(<Button onClick={mockOnClick}>Click</Button>); await userEvent.setup().click(screen.getByRole('button')); expect(mockOnClick).toHaveBeenCalledTimes(1); - Mock an entire module with jest.mock.
jest.mock('./api', () => ({ fetchUser: jest.fn(() => Promise.resolve({ name: 'Alice' })) })); - Spy on a module method without replacing it entirely.
import * as api from './api'; jest.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Bob' }); - Reset mocks between tests to avoid leaking state.
afterEach(() => { jest.clearAllMocks(); // resets call counts and instances }); - Mock timers to test debounced or delayed behavior.
jest.useFakeTimers(); render(<Debounced />); await userEvent.setup().type(input, 'hello'); jest.advanceTimersByTime(300); // skip the debounce delay expect(screen.getByText('hello')).toBeInTheDocument();
React Testing
(FAQ)FAQ
userEvent simulates real browser interactions more accurately — it triggers the full sequence of events (pointerdown, focus, input, click, etc.) that a real user would cause, while fireEvent dispatches a single synthetic event. Prefer userEvent for most interaction tests.
Use findBy queries (e.g., findByRole, findByText) which return a promise and automatically wait for the element to appear. Alternatively, wrap your assertion in waitFor(() => expect(...)) for more complex async scenarios.
Mock the fetch or axios module using jest.mock, or intercept requests with a library like msw (Mock Service Worker). Return controlled responses so your test verifies how the component handles success and error states without making real network calls.
Querying by test ID only works in tests and tells you nothing about what users actually experience. Prefer getByRole or getByText, which validate accessibility and visible content — they catch regressions that matter to users, not just DOM structure.
Pass a jest.fn() as the onSubmit prop, use userEvent.type to fill in fields, then userEvent.click the submit button. Assert with expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ fieldName: 'value' })).
React Testing Best Practices
Test React components effectively using React Testing Library, Vitest, and common testing patterns.
TL;DR
- 01Test user behavior, not implementation details.
- 02Use React Testing Library to query elements as users see them.
- 03Mock external dependencies but test component logic thoroughly.
Tips
- 01Test user behavior and critical paths thoroughly, even if it means longer tests — catching real bugs is more important than test speed.
Warnings
- 01Avoid testing implementation details like component state or internal functions, since refactoring will break tests that depend on the old structure.
React Testing Best Practices
(continued)Testing User Behavior
- Test what users see and do, not how components are built.
import { render, screen } from "@testing-library/react"; import Button from "./Button"; it("shows clicked message when button is clicked", async () => { render(<Button />); const button = screen.getByRole("button", { name: /click me/i }); await userEvent.click(button); expect(screen.getByText("You clicked!")).toBeInTheDocument(); }); - Query elements like users would: by text, role, label, not by className.
// Good: user sees this text screen.getByText("Welcome"); screen.getByRole("button", { name: /submit/i }); screen.getByLabelText("Email"); // Bad: implementation detail screen.getByTestId("submit-btn"); - Avoid testing component state or internal functions.
- Focus on inputs, outputs, and user interactions.
React Testing Best Practices
(continued)Setup and Utilities
- Configure Vitest with React Testing Library setup.
// vitest.config.js import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom" } }); - Create a custom render function to set up providers.
import { render } from "@testing-library/react"; export function renderWithProviders(ui) { return render(<ThemeProvider>{ui}</ThemeProvider>); } - Use this custom render in all tests to reduce boilerplate.
it("applies theme colors", () => { renderWithProviders(<Component />); // Component now has theme context });
React Testing Best Practices
(continued)Mocking Dependencies
- Mock external API calls to keep tests fast and isolated.
import { vi } from "vitest"; vi.mock("./api", () => ({ fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: "Alice" }) ) })); - Mock Next.js router for navigation testing.
vi.mock("next/router", () => ({ useRouter: vi.fn(() => ({ push: vi.fn(), pathname: "/" })) })); - Avoid mocking everything — only mock slow or external things.
- Test component logic thoroughly with real implementations.
React Testing Best Practices
(continued)Async Testing
- Use async/await for testing async code like API calls.
it("displays user data after fetching", async () => { render(<UserProfile userId="1" />); // Component fetches user data const name = await screen.findByText("Alice"); expect(name).toBeInTheDocument(); }); - Use findBy for elements that appear after async operations.
// findBy waits for the element to appear const element = await screen.findByText("Loaded"); // getBy fails immediately if element doesn't exist expect(() => screen.getByText("Loaded")).toThrow(); - Use waitFor for complex async scenarios.
await waitFor(() => { expect(screen.getByText("Success")).toBeInTheDocument(); });
React Testing Best Practices
(continued)Common Testing Patterns
- Test form submission with userEvent.
it("submits form with email", async () => { const user = userEvent.setup(); render(<LoginForm />); await user.type(screen.getByLabelText("Email"), "test@example.com"); await user.type(screen.getByLabelText("Password"), "password"); await user.click(screen.getByRole("button", { name: /login/i })); expect(screen.getByText("Welcome")).toBeInTheDocument(); }); - Test conditional rendering based on props.
it("shows success message when isSuccess is true", () => { render(<Alert isSuccess={true} message="All good!" />); expect(screen.getByText("All good!")).toBeInTheDocument(); }); - Test error handling and edge cases.
it("shows error when API fails", async () => { vi.mocked(fetchUser).mockRejectedValue(new Error("API failed")); render(<UserProfile userId="1" />); expect(await screen.findByText("Error loading user")).toBeInTheDocument(); });
React Testing Best Practices
(FAQ)FAQ
React Testing Library is the current standard and is officially recommended by the React team. Enzyme is largely unmaintained and encourages querying internal state and component structure, which makes tests fragile when you refactor.
Use findBy* queries (e.g., await screen.findByText('Loaded')) or wrap assertions in await waitFor(() => expect(...)) after triggering the action. For realistic network mocking across many tests, consider Mock Service Worker (msw) instead of per-test fetch stubs.
userEvent simulates the full sequence of browser events a real user triggers (e.g., pointerover, focus, keydown, input, keyup on a type), while fireEvent dispatches a single synthetic event. Use userEvent from @testing-library/user-event for interactions like typing and clicking to catch bugs that depend on event ordering.
Call jest.mock('axios') at the top of your test file, then configure return values with axios.get.mockResolvedValue({ data: yourData }) per test. Reset mocks between tests using jest.clearAllMocks() in a beforeEach to prevent state from leaking across test cases.
Use getBy* when the element must exist synchronously (throws if missing), queryBy* when asserting an element is absent (returns null instead of throwing), and findBy* for elements that appear asynchronously (returns a promise). Prefer queries in this priority order: ByRole, ByLabelText, ByPlaceholderText, ByText — they mirror how users and assistive technology discover elements.
React useContext Advanced Patterns
Advanced patterns for Context API, custom hooks, and state management.
TL;DR
- 01Use useSyncExternalStore to build fine-grained context subscriptions that skip unnecessary re-renders.
- 02Wrap components in a provider wrapper in tests to keep context consumers testable in isolation.
- 03Client-only context does not work in Server Components — pass data via props or use cookies/headers.
Tips
- 01Export a createTestWrapper function from your test utils that returns a wrapper component — this integrates cleanly with React Testing Library's wrapper option.
- 02Always expose async operations through the custom hook, not raw context — it keeps the async logic centralized.
Warnings
- 01This pattern requires careful implementation — prefer Zustand or Jotai in production for battle-tested selector behavior without the boilerplate.
- 02Never import a Server Component inside a Client Component — this breaks the React Server Component model. Server Components can receive Client Providers as children though.
- 03Don't over-fetch in Context providers — prefer React Query for server data and reserve Context for client-side state.
React useContext Advanced Patterns
(continued)Custom Context Hooks
Wrap
useContextin a custom hook to simplify access and centralise the context logic.const UserContext = createContext<{ user: User | null; setUser: (u: User | null) => void } | undefined>(undefined); export function UserProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); } export function useUser() { const context = useContext(UserContext); if (!context) throw new Error("useUser must be used inside UserProvider"); return context; }Throw a descriptive error in the hook when the context is missing to catch misconfigured trees early.
export function useTheme() { const ctx = useContext(ThemeContext); if (ctx === undefined) { throw new Error("useTheme must be called inside ThemeProvider"); } return ctx; }Add derived values inside the hook so consumers never repeat the same logic.
export function useAuth() { const { user } = useContext(AuthContext)!; return { user, isLoggedIn: !!user, // derived isAdmin: user?.role === "admin", // derived displayName: user?.name ?? "Guest", // derived }; }Keep the raw context private — only export the provider and the custom hook.
// auth-context.tsx const AuthContext = createContext<AuthState | undefined>(undefined); // NOT exported export function AuthProvider({ children }: { children: React.ReactNode }) { /* ... */ } export function useAuth() { return useContext(AuthContext)!; } // only hook exportedExport a single hook per context so consumers never call
useContextdirectly.// consumers call the hook, not useContext — implementation stays hidden function Profile() { const { user, isAdmin } = useAuth(); // clean, type-safe API return <div>{isAdmin ? "Admin: " : ""}{user?.name}</div>; }
React useContext Advanced Patterns
(continued)Context Selectors
Use
useSyncExternalStoreto build a selector pattern that only re-renders consumers when their selected slice changes.import { createContext, useContext, useRef, useSyncExternalStore } from "react"; type Store<T> = { get: () => T; set: (val: Partial<T>) => void; subscribe: (cb: () => void) => () => void }; function createStore<T>(initial: T): Store<T> { let state = initial; const listeners = new Set<() => void>(); return { get: () => state, set: (partial) => { state = { ...state, ...partial }; listeners.forEach(l => l()); }, subscribe: (cb) => { listeners.add(cb); return () => listeners.delete(cb); } }; } const StoreContext = createContext<Store<{ count: number; user: string }>>(null!); // Selector hook — only re-renders when the selected value changes function useStore<R>(selector: (state: { count: number; user: string }) => R): R { const store = useContext(StoreContext); return useSyncExternalStore(store.subscribe, () => selector(store.get())); } // Only re-renders when count changes, ignores user changes function Counter() { const count = useStore(s => s.count); return <p>{count}</p>; }This pattern mimics Zustand's selector behavior inside React Context — useful when you want React Context ergonomics without an external library.
Compare to naive Context: without selectors, every consumer re-renders on any context change; with selectors, only consumers whose selected value changed re-render.
React useContext Advanced Patterns
(continued)Testing Context Providers
Wrap components in their providers inside a test render helper so each test controls exactly what context the component receives.
// test-utils.tsx import { render } from "@testing-library/react"; import { UserProvider } from "../contexts/UserContext"; import { ThemeProvider } from "../contexts/ThemeContext"; export function renderWithProviders(ui: React.ReactElement, { user = null, theme = "light" } = {}) { return render( <UserProvider initialUser={user}> <ThemeProvider initialTheme={theme}> {ui} </ThemeProvider> </UserProvider> ); }Pass initial values to providers via props so tests can control state without mocking global modules.
// In your provider, accept initial state as a prop export function UserProvider({ children, initialUser = null }) { const [user, setUser] = useState(initialUser); return <UserContext.Provider value={{ user, setUser }}>{children}</UserContext.Provider>; }Test components that consume context by rendering them through the helper.
test("shows welcome message for logged-in user", () => { const { getByText } = renderWithProviders(<Navbar />, { user: { name: "Alice", role: "admin" } }); expect(getByText("Welcome, Alice")).toBeInTheDocument(); });Test the provider itself by rendering a consumer inside it and interacting with it.
test("login updates user context", async () => { const { getByRole, findByText } = render( <UserProvider><LoginForm /><Navbar /></UserProvider> ); await userEvent.click(getByRole("button", { name: "Login" })); expect(await findByText("Welcome, Alice")).toBeInTheDocument(); });Avoid mocking
useContextdirectly — testing through real providers verifies that the full provider+consumer integration works correctly.
React useContext Advanced Patterns
(continued)Context and Server Components
React Context requires a client component — Server Components cannot call
useContextor render a<Provider>.// app/layout.tsx — Server Component (default in App Router) // Cannot use createContext or useContext here directly export default function RootLayout({ children }) { return <html><body><ClientProviders>{children}</ClientProviders></body></html>; }Extract providers into a dedicated client component so the root layout stays a Server Component.
// app/providers.tsx "use client"; import { ThemeProvider } from "./contexts/ThemeContext"; import { UserProvider } from "./contexts/UserContext"; export function ClientProviders({ children }: { children: React.ReactNode }) { return ( <ThemeProvider> <UserProvider>{children}</UserProvider> </ThemeProvider> ); }Pass initial data from Server Components to Context providers via props — fetch on the server, hydrate on the client.
// app/layout.tsx (Server Component) import { getUser } from "@/lib/auth"; import { ClientProviders } from "./providers"; export default async function RootLayout({ children }) { const user = await getUser(); // server-side fetch return ( <html><body> <ClientProviders initialUser={user}>{children}</ClientProviders> </body></html> ); }Use
cookies()orheaders()fromnext/headersin Server Components instead of Context for data that doesn't need client reactivity.import { cookies } from "next/headers"; export default async function Page() { const theme = (await cookies()).get("theme")?.value ?? "light"; return <div data-theme={theme}>...</div>; }Server Components can be children of Client Context providers — they receive the context value when they re-render on the client.
React useContext Advanced Patterns
(continued)Context with Async Operations
Define an async login function inside the provider and expose it through the context value.
export function UserProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(false); const login = async (email: string, password: string) => { setLoading(true); try { const result = await authenticate(email, password); // API call setUser(result); } finally { setLoading(false); } }; return ( <UserContext.Provider value={{ user, login, loading }}> {children} </UserContext.Provider> ); }Store
loadinganderroralongside data in the context value so every consumer can react to async state.const [state, setState] = useState<{ data: User | null; loading: boolean; error: string | null; }>({ data: null, loading: false, error: null }); // Expose all three so consumers render loading/error UI without local state <DataContext.Provider value={state}>{children}</DataContext.Provider>Dispatch async actions through
useReducerto keep state transitions predictable and testable.type Action = | { type: "FETCH_START" } | { type: "FETCH_SUCCESS"; payload: User } | { type: "FETCH_ERROR"; payload: string }; const [state, dispatch] = useReducer(asyncReducer, initialState); async function fetchUser(id: string) { dispatch({ type: "FETCH_START" }); try { const user = await getUser(id); dispatch({ type: "FETCH_SUCCESS", payload: user }); } catch (e) { dispatch({ type: "FETCH_ERROR", payload: (e as Error).message }); } }Cancel in-flight async operations on unmount to prevent state updates on unmounted providers.
useEffect(() => { const controller = new AbortController(); async function load() { try { const res = await fetch("/api/user", { signal: controller.signal }); setUser(await res.json()); } catch (e) { if ((e as Error).name !== "AbortError") setError("Failed to load"); } } load(); return () => controller.abort(); // cancel on unmount }, []);Expose async methods via the custom hook so callers never interact with raw context directly.
export function useUser() { const ctx = useContext(UserContext); if (!ctx) throw new Error("useUser must be used inside UserProvider"); // Wrap async method to keep error handling in one place const safeLogin = async (email: string, password: string) => { await ctx.login(email, password); }; return { ...ctx, login: safeLogin }; }
React useContext Advanced Patterns
(FAQ)FAQ
Split your context by domain so components only subscribe to the slice of state they need. For example, keep user auth in one context and theme preferences in another — updating auth won't re-render theme consumers.
Reach for useReducer when your context state has multiple sub-values that change together or when state transitions depend on previous state. It also makes it easier to share both state and dispatch through the same context.
Pass an initializer function as the second argument to useReducer, or compute the initial value inside a useState initializer function — this runs only once on mount rather than on every render.
Create a custom hook like useAuth() that calls useContext internally and throws a descriptive error if used outside the provider. This catches misconfigured component trees early and keeps consumer code clean.
Store both data and loading/error status in the context state, then trigger fetches via useEffect inside the provider component. Expose the status alongside the data so consumers can react to loading and error states without managing them locally.
React useContext Hook
Share state across React components without prop drilling using the useContext hook.
TL;DR
- 01Create a Context with createContext for shared data.
- 02Wrap components with a Provider to make data available.
- 03Use useContext to access data in any child component.
Tips
- 01Create custom hooks for contexts to simplify usage and add error checking for consumers.
Warnings
- 01Every context change causes all consumers to re-render — split contexts by concern to avoid performance issues.
React useContext Hook
(continued)Creating Context
- Create a context to hold shared data.
const ThemeContext = React.createContext(); - Provide an initial value as a fallback when no provider wraps the consumer.
const UserContext = React.createContext({ user: null, setUser: () => {} }); - Export the context for use in other components.
export const ThemeContext = React.createContext(); - Keep context creation in its own file for clarity.
// theme-context.js import { createContext } from "react"; export const ThemeContext = createContext("light"); - Use TypeScript generics to type the context value.
interface User { id: number; name: string; } interface UserContextType { user: User | null; setUser: (u: User) => void; } const UserContext = createContext<UserContextType | undefined>(undefined);
React useContext Hook
(continued)Creating a Provider
- Wrap your app with a Provider component.
function App() { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> <Header /> <Main /> <Footer /> </ThemeContext.Provider> ); } - Provider passes value to all child components.
function Layout({ children }) { const [user, setUser] = useState(null); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); } - Extract the provider into its own component to keep App clean.
export function ThemeProvider({ children }) { const [theme, setTheme] = useState("light"); return ( <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> ); } - Memoize the context value to avoid re-rendering every consumer.
function UserProvider({ children }) { const [user, setUser] = useState(null); const value = useMemo(() => ({ user, setUser }), [user]); return ( <UserContext.Provider value={value}> {children} </UserContext.Provider> ); } - Nest multiple providers at the top of the tree.
function App() { return ( <AuthProvider> <ThemeProvider> <Router> <AppRoutes /> </Router> </ThemeProvider> </AuthProvider> ); }
React useContext Hook
(continued)Consuming Context
- Access context with useContext hook.
function Header() { const { theme, setTheme } = useContext(ThemeContext); return ( <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}> Current theme: {theme} </button> ); } - useContext works in any descendant component, no matter how deep.
function DeepComponent() { const { user } = useContext(UserContext); return <p>User: {user?.name}</p>; } - Returns the createContext default value if no matching Provider is above it, or undefined if no default was set.
- Destructure only the values you need to be explicit.
// Clear: names the exact values this component uses const { theme } = useContext(ThemeContext); - Re-renders happen whenever the context value changes.
// This component re-renders every time theme or setTheme changes const { theme, setTheme } = useContext(ThemeContext);
React useContext Hook
(continued)Custom Hooks
- Create a custom hook to simplify context access.
function useTheme() { const context = useContext(ThemeContext); if (!context) { throw new Error("useTheme must be inside ThemeProvider"); } return context; } function Header() { const { theme, setTheme } = useTheme(); return <button onClick={() => setTheme("dark")}>{theme}</button>; } - Custom hooks provide error checking and a cleaner API.
- Makes consuming context easier for other developers.
- Add computed values or helpers inside the custom hook.
function useAuth() { const { user } = useContext(AuthContext); return { user, isLoggedIn: !!user, isAdmin: user?.role === "admin" }; } - Export only the custom hook, not the raw context.
// users import useUser, not UserContext directly export { useUser }; // export hook // don't export UserContext — keep it private
React useContext Hook
(continued)Best Practices
- Place the Provider as close as possible to the components that need it, not always at the app root.
// Scope SidebarContext to the sidebar, not the whole app function Sidebar() { return ( <SidebarContext.Provider value={collapsed}> <SidebarPanel /> </SidebarContext.Provider> ); } - Colocate context with the feature that owns it instead of lifting everything to a shared global file — move it up only when a second, unrelated feature genuinely needs the same data.
// features/cart/cart-context.jsx — lives next to the feature, not in a top-level contexts/ folder export const CartContext = createContext(null); - Avoid context for state that changes on every keystroke or frame, like form input or animation values — high-frequency updates re-render every consumer and are a poor fit for the context model.
// Fast-changing values fit local state or a state library better than context const [query, setQuery] = useState(""); // keep local, not in context - Prefer local state or props for data used by only 1–2 components.
// Don't reach for Context just to avoid one prop // Context is useful for truly global data: auth, theme, locale - Test context consumers by wrapping them in the provider, not by mocking the context module.
test("shows username from context", () => { render( <UserContext.Provider value={{ user: { name: "Alice" }, setUser: jest.fn() }}> <UserDisplay /> </UserContext.Provider> ); expect(screen.getByText("Alice")).toBeInTheDocument(); }); - Build a small reusable test helper that wraps render with your real providers, so every test exercises the same provider tree your app uses in production.
function renderWithProviders(ui) { return render(<AuthProvider><ThemeProvider>{ui}</ThemeProvider></AuthProvider>); }
React useContext Hook
(FAQ)FAQ
Use useContext when data needs to be accessed by many components at different nesting levels, such as theme, locale, or auth state. Prop drilling becomes a maintenance burden once you're passing props through more than 2-3 layers of components that don't use the data themselves.
All consumers re-render whenever any value in the context object changes. Split your context into smaller, focused contexts by concern (e.g., separate AuthContext from ThemeContext) so that a change in one doesn't trigger re-renders in unrelated consumers.
Include an updater function in your context value alongside the data — pass a useState setter or a dispatch function from useReducer. Consumers can then call that function to trigger state changes that propagate back through the Provider.
useContext is built-in and ideal for low-frequency updates like theme or auth, with no extra dependencies. Redux adds middleware, devtools, and a strict unidirectional flow suited to complex, high-frequency global state — useContext alone can become hard to scale when many unrelated pieces of state share a single context.
In your custom hook, check that the returned context value is not undefined and throw a descriptive error if it is — for example: if (!ctx) throw new Error('useAuth must be used within an AuthProvider'). This gives a clear error instead of a silent undefined crash deep in the component tree.
React useEffect Hook
Handle side effects, manage dependencies, and clean up after effects.
TL;DR
- 01useEffect runs code after render for side effects like fetching data.
- 02Dependency array controls when the effect runs.
- 03Return a cleanup function to prevent memory leaks.
Tips
- 01Always include all dependencies in the dependency array — ESLint's exhaustive-deps rule helps catch missing ones.
Warnings
- 01Forgetting cleanup functions causes memory leaks — always clean up subscriptions, timers, and listeners.
React useEffect Hook
(continued)Basic useEffect
Run a side effect after every render by calling
useEffectwith no dependency array.useEffect(() => { console.log("Component rendered"); // runs after every render });Update the document title after each render as a common side-effect pattern.
useEffect(() => { document.title = `You clicked ${count} times`; // Runs after every render where count may have changed });Wrap async logic in an inner function because the effect callback itself cannot be async.
useEffect(() => { async function loadData() { const res = await fetch("/api/users"); const data = await res.json(); setUsers(data); } loadData(); // call the async function immediately }, []);Use
useEffectfor imperative side effects like logging, analytics, or third-party integrations.useEffect(() => { analytics.track("page_view", { page: pathname }); // fire-and-forget side effect }, [pathname]); // re-fires when the route changesKeep effect callbacks synchronous at the top level and wrap any async work inside.
// Wrong: async effect callback — cleanup return value is lost useEffect(async () => { await doSomething(); }, []); // Right: inner async function with explicit call useEffect(() => { async function run() { await doSomething(); } run(); }, []);
React useEffect Hook
(continued)Dependency Comparison Rules
Know that React compares each dependency to its previous value with
Object.is, not a deep equality check — this is the exact mechanism that decides whether an effect re-runs.useEffect(() => { console.log("Count changed:", count); // Object.is(prevCount, count) === false triggers a re-run }, [count]);Expect object and array literals to fail
Object.isevery render because a new reference is created each time, even when the contents are identical.useEffect(() => { fetchUserData(filters); // filters = { status: "active" } looks "changed" every render }, [filters]); // new object reference each render → effect re-fires constantlyFix reference-instability by memoizing the object/array with
useMemoor by destructuring to primitive dependencies instead.useEffect(() => { fetchUserData(status, token); // primitives compare correctly with Object.is }, [status, token]); // both must appear in the deps arrayOmit the array entirely only when the effect must run after every single render — each item is still diffed with
Object.is, there's just no array to skip the check.useEffect(() => { // Runs after EVERY render — use only when you truly need this syncStateToExternalSystem(value); }); // no array = every renderEnable the ESLint
react-hooks/exhaustive-depsrule so it statically flags any variable read inside the effect that's missing from the array — it does not understandObject.issemantics, only static usage.# Install the rules-of-hooks plugin npm install eslint-plugin-react-hooks --save-dev # Add to .eslintrc: "react-hooks/exhaustive-deps": "warn" # Use the rule's quick-fix to auto-insert missing deps, then verify literals are memoizedAvoid silencing
exhaustive-depswith an inline disable comment — it almost always hides a stale-closure bug rather than a false positive.useEffect(() => { fetchUserData(userId); // eslint-disable-next-line react-hooks/exhaustive-deps // Tempting but dangerous: userId changes won't re-trigger this effect }, []);
React useEffect Hook
(continued)Data Fetching
Fetch data when the component mounts by using an empty dependency array.
useEffect(() => { fetch("/api/data") .then((r) => r.json()) .then(setData); }, []); // runs once on mountTrack
loadinganderrorstate alongside the fetch for complete async UI handling.useEffect(() => { setLoading(true); fetch(url) .then((r) => r.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)); }, [url]); // re-fetches when url changesCancel stale requests with
AbortControllerto prevent setting state after unmount.useEffect(() => { const controller = new AbortController(); fetch(url, { signal: controller.signal }) .then((r) => r.json()) .then(setData) .catch((err) => { if (err.name !== "AbortError") setError(err); // ignore intentional cancellation }); return () => controller.abort(); // cancel on unmount or url change }, [url]);Re-fetch automatically when a dependency like
userIdchanges by including it in the array.useEffect(() => { async function load() { const res = await fetch(`/api/users/${userId}`); setUser(await res.json()); } load(); }, [userId]); // re-runs every time userId changesUse SWR or React Query in production for caching, deduplication, and automatic revalidation.
import useSWR from "swr"; // SWR handles loading state, caching, and revalidation on focus const { data, error, isLoading } = useSWR("/api/user", fetcher);
React useEffect Hook
(continued)Cleanup Functions
Return a function from the effect callback itself — this is the only way React knows it's a cleanup function, and it fires before unmount or before the next effect run.
useEffect(() => { const subscription = subscribe((data) => { setData(data); }); return () => subscription.unsubscribe(); // cleanup before unmount }, []);Remember that the cleanup function closes over the props/state values from the render that scheduled it, not the latest render — it always sees the OLD values, never the current ones.
useEffect(() => { const id = roomId; // captured by this closure connect(id); return () => disconnect(id); // disconnects the OLD roomId, even after roomId changes }, [roomId]);Picture the exact ordering on every dependency change: React runs the PREVIOUS effect's cleanup first, fully, before invoking the NEW effect's setup — never interleaved.
useEffect(() => { const timer = setInterval(() => setTime((t) => t + 1), 1000); return () => clearInterval(timer); // old timer fully cleared before a new one starts }, [intervalMs]); // change in intervalMs: cleanup(old) → setup(new), in that orderPair every subscription-style API call with its exact inverse in cleanup — addEventListener/removeEventListener, connect/disconnect, open/close — so each effect run leaves zero residue for the next one to build on.
useEffect(() => { const handleResize = () => setWidth(window.innerWidth); window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); // exact inverse of setup }, []);Treat a missing cleanup return as a silent bug, not a no-op — React still re-runs the new effect on every dependency change, it just never undoes the old one first.
useEffect(() => { const ws = new WebSocket(`wss://example.com/${roomId}`); ws.onmessage = (e) => setMessages((prev) => [...prev, e.data]); return () => ws.close(); // without this, every roomId change leaks an open socket }, [roomId]);Re-subscribe correctly when a dependency like
userIdchanges by relying on cleanup to unsubscribe from the PREVIOUS value before the new effect subscribes to the current one.useEffect(() => { const sub = subscribe(userId); // subscribe to new user return () => sub.unsubscribe(); // unsubscribe from PREVIOUS user first }, [userId]); // runs cleanup on every userId change before re-subscribing
React useEffect Hook
(continued)useEffect Patterns
Sync state with
localStorageby reading on mount and writing when state changes.useEffect(() => { const saved = localStorage.getItem("theme"); if (saved) setTheme(saved); // restore on mount }, []); useEffect(() => { localStorage.setItem("theme", theme); // persist on every change }, [theme]);Track the previous value of a variable by storing it in a ref before each update.
const prevCountRef = useRef<number | undefined>(undefined); useEffect(() => { prevCountRef.current = count; // store current as previous after each render }, [count]); const prevCount = prevCountRef.current; // previous value available during renderUse multiple separate
useEffectcalls for unrelated concerns to keep each effect focused.useEffect(() => { /* handle auth token refresh */ }, [token]); useEffect(() => { /* sync theme to body class */ }, [theme]); useEffect(() => { /* track window resize */ }, []); // Three concerns → three effects, each independently managedFocus an input or dialog element after it becomes visible in the DOM.
useEffect(() => { if (isOpen && inputRef.current) { inputRef.current.focus(); // focus runs after the DOM updates } }, [isOpen]); // re-runs whenever isOpen toggles to trueUpdate the document title reactively to reflect dynamic values like unread counts.
useEffect(() => { document.title = unreadCount > 0 ? `(${unreadCount}) My App` : "My App"; }, [unreadCount]); // updates whenever unreadCount changesTreat Strict Mode's extra dev-only mount cycle as a debugging aid, not a bug, when writing custom hooks or subscriptions.
function useChatRoom(roomId) { useEffect(() => { const connection = createConnection(roomId); connection.connect(); return () => connection.disconnect(); // must fully undo connect() }, [roomId]); // Strict Mode connects, disconnects, reconnects in dev to prove cleanup is correct }
React useEffect Hook
(FAQ)FAQ
useEffect runs after the browser has painted the screen, not during render. This makes it safe for DOM mutations and async operations without blocking the UI.
In development, React 18+ Strict Mode intentionally runs setup, cleanup, then setup again for every effect on mount — even with an empty dependency array. This is not a bug: it simulates remounting to surface effects with missing or broken cleanup. Production builds always run the effect once. Fix the underlying issue (incomplete cleanup) rather than trying to suppress the double run.
An infinite loop usually means an object or array is listed as a dependency but gets recreated on every render, so Object.is sees a new reference each time. Move the value outside the component, memoize it with useMemo/useCallback, or restructure so only primitives are in the dependency array.
useLayoutEffect fires synchronously after DOM mutations but before the browser paints, so it blocks rendering — use it only when you need to measure or mutate the DOM before the user sees it. Prefer useEffect for everything else.
Use an AbortController: create it at the top of the effect, pass signal to fetch, and call controller.abort() in the cleanup function. This prevents setState from being called on an unmounted component when the request resolves late.
React useMemo and useCallback
Optimize React performance by memoizing values and callbacks to prevent unnecessary re-renders.
TL;DR
- 01Use useMemo to cache expensive calculations between renders.
- 02Use useCallback to keep function references stable for optimization.
- 03Pass a dependency array to control when memoization resets.
Tips
- 01Use the React DevTools Profiler to measure actual performance gains before and after memoization, since it adds overhead.
Warnings
- 01Missing dependencies in the array causes stale closures and subtle bugs, so enable the ESLint rule to catch them automatically.
React useMemo and useCallback
(continued)useMemo Basics
- Import
useMemofrom React at the top of your component file.import { useMemo } from "react"; - Wrap an expensive calculation to cache its result between renders.
const total = useMemo(() => { return items.reduce((sum, item) => sum + item.price, 0); }, [items]); - The hook returns the cached value, not the function itself.
- Use a dependency array to reset the cache when inputs change.
- Only use
useMemowhen the calculation is slow and frequent.
React useMemo and useCallback
(continued)useCallback Basics
- Import
useCallbackfrom React at the top of your component file.import { useCallback } from "react"; - Memoize a callback function so its reference stays the same between renders.
const handleClick = useCallback(() => { console.log("clicked"); }, []); - Pass the memoized function to child components expecting stable references.
- Use a dependency array that includes every value the callback reads.
- The reference only changes when a dependency changes.
React useMemo and useCallback
(continued)Dependency Arrays
- An empty dependency array
[]memoizes forever, never resetting.const value = useMemo(() => expensive(), []); - Include values that the calculation depends on in the array.
const filtered = useMemo(() => filter(items, search), [items, search]); - The cache resets when any dependency changes, then recalculates fresh.
- Forgetting a dependency can cause stale values and hard-to-find bugs.
- Use ESLint rules to catch missing dependencies automatically.
React useMemo and useCallback
(continued)When to Use Them
- Use
useMemoonly for calculations that noticeably slow down rendering. - Use
useCallbackwhen passing functions to optimized child components. - Use
React.memo()alongsideuseCallbackfor component memoization. - Avoid premature optimization — measure first with DevTools profiler.
- Do not memoize simple values or callbacks — the overhead costs more.
React useMemo and useCallback
(continued)Common Patterns
- Memoize object literals passed as props to prevent child re-renders.
const config = useMemo(() => ({ timeout: 5000, retries: 3 }), []); - Use
useCallbackwithuseEffectto avoid infinite loops.const fetch = useCallback(async () => { const res = await api.get(); setData(res); }, []); - Combine with
useMemoto memoize complex derived state. - Watch the React DevTools profiler to confirm memoization helps.
- Consider using a state management library for complex memoization needs.
React useMemo and useCallback
(FAQ)FAQ
useMemo caches the return value of a function, while useCallback caches the function itself. Use useMemo for expensive computations and useCallback when you need a stable function reference to pass as a prop or dependency.
Only memoize when the computation is genuinely expensive (e.g., filtering large arrays, complex math) and runs on frequent re-renders. For simple transformations, the overhead of memoization outweighs any benefit.
This is a stale closure caused by missing dependencies in the dependency array. Any variable from the component scope that the function references must be listed in the array, or the callback will capture its initial value indefinitely.
No — it can actually hurt performance by adding memory and comparison overhead. useCallback is only beneficial when passing functions to memoized child components (React.memo) or when the function is listed as a dependency in another hook like useEffect.
List each prop used in the computation as a dependency: const result = useMemo(() => expensiveCalc(propA, propB), [propA, propB]). The memoized value recomputes only when those specific props change, not on every render.
React useReducer Hook
Manage complex state logic with the useReducer hook, actions, reducers, and common patterns.
TL;DR
- 01Use useReducer for state with several related update rules.
- 02Write a reducer function that handles each action type.
- 03Dispatch actions to trigger state changes in components.
Tips
- 01Pair <code>useReducer</code> with <code>useContext</code> to build a clean global store without pulling in Redux or Zustand for small apps.
Warnings
- 01Never mutate the state object directly inside a reducer, since React relies on a new reference to detect changes and re-render.
React useReducer Hook
(continued)Basic Usage
- Import
useReducerfrom React at the top of your component file.import { useReducer } from "react"; - Call
useReducerwith a reducer function and an initial state value.const [state, dispatch] = useReducer(reducer, { count: 0 }); - The hook returns the current state and a dispatch function for actions.
- Reducers are pure functions that take state and an action, then return new state.
- Use this pattern when state updates depend on the current state.
React useReducer Hook
(continued)Writing Reducers
- Define a reducer function that handles each action type with a switch.
function reducer(state, action) { switch (action.type) { case "increment": return { count: state.count + 1 }; case "decrement": return { count: state.count - 1 }; default: return state; } } - Always return a new state object instead of mutating the existing one.
- Include a default case to handle unknown action types safely.
- Keep reducers pure — no API calls, timers, or random values inside.
- This makes state changes predictable and easy to test.
React useReducer Hook
(continued)Dispatching Actions
- Call
dispatchwith an action object to trigger a state update.<button onClick={() => dispatch({ type: "increment" })}> + </button> - Actions are plain objects with a required
typefield. - Add extra fields to pass data along with the action.
dispatch({ type: "add", payload: 5 }); - The reducer reads these fields to compute the new state.
- Dispatch can be passed down to child components through props.
React useReducer Hook
(continued)When to Use It
- Reach for
useReducerwhen state has many related update paths. - Use it when the next state depends on multiple pieces of current state.
- Use it when actions need to be predictable and easy to test.
- Use
useStatefor simple booleans, strings, and counters instead. - Use Context plus reducer for app-wide state without external libraries.
React useReducer Hook
(continued)Common Patterns
- Initialize lazily with a third argument for expensive setup logic.
const [state, dispatch] = useReducer(reducer, props, (p) => ({ count: p.initial })); - Type actions with a discriminated union in TypeScript for safety.
type Action = | { type: "increment" } | { type: "add"; payload: number }; - Pair
useReducerwithuseContextto share state across the tree. - Split large reducers into smaller helper functions for clarity.
- Use
useImmerReducerfrom libraries when deep updates feel tedious.
React useReducer Hook
(FAQ)FAQ
Reach for useReducer when your state has multiple sub-values that change together, or when the next state depends on the previous one in complex ways. If you find yourself writing several related useState calls or passing many setters down as props, useReducer is likely the cleaner choice.
It takes a reducer function and an initial state value: const [state, dispatch] = useReducer(reducer, initialState). An optional third argument lets you pass an init function to lazily compute the initial state, which is useful for expensive setup.
Use a switch statement on action.type, returning a new state object for each case and a default that returns the current state unchanged. Each case should spread the existing state and override only the fields that change: return { ...state, count: state.count + 1 }.
For small-to-medium apps it often can — combine useReducer with useContext to share state across the tree without a third-party library. Redux still has advantages for large apps that need DevTools time-travel debugging, middleware, or a single store across many independent feature modules.
React compares state by reference, so if you return the same object (or array) the component won't re-render. Always return a new object from every reducer branch — use the spread operator or Object.assign instead of modifying the existing state in place.
React Component Patterns
Build scalable component architectures with established design patterns.
TL;DR
- 01Design controlled component APIs so parents own state via value and onChange props.
- 02Use the Provider Pattern to share state across a subtree without prop drilling.
- 03Pick compound components for closed sub-component sets, render props when callers control markup.
Tips
- 01If your component accepts both value (controlled) and defaultValue (uncontrolled), warn in development when a consumer switches between the two modes — React's own inputs do this and it prevents subtle bugs.
- 02Memoize the context value with useMemo when the provider re-renders frequently — otherwise every consumer re-renders even when the value has not changed.
- 03Export compound sub-components as static properties of the parent so the public API stays a single cohesive import instead of several loose named exports.
- 04When your container grows complex, extract it into a custom hook that returns { users, loading, error } — this keeps the JSX layer thin and moves all async logic to a testable function.
Warnings
- 01Avoid nesting render props three levels deep or overusing Context — both create tight coupling that makes components harder to test and reuse in isolation.
React Component Patterns
(continued)Controlled Component API
A controlled component exposes a value prop and an onChange callback so the parent owns the state entirely. This is React's "single source of truth" contract — the component never holds private internal state for the value it displays.
// Controlled input component
function TextInput({ value, onChange, placeholder }) {
return (
<input
type="text"
value={value}
placeholder={placeholder}
onChange={(e) => onChange(e.target.value)}
/>
);
}
// Parent owns the state
function LoginForm() {
const [email, setEmail] = useState('');
return (
<TextInput
value={email}
onChange={setEmail}
placeholder="Email address"
/>
);
}
- Fully controlled: parent passes
valueandonChange— no internal state for the value. - Uncontrolled fallback: omit
valueand use arefviauseReffor one-time reads (e.g., file inputs). - Validation and transformation happen in the parent's
onChangehandler before state is updated.
React Component Patterns
(continued)Provider Pattern
The Provider Pattern uses React Context to broadcast shared state — such as theme, locale, or authenticated user — to any descendant without threading props through every intermediate component. Unlike compound components, the consumers can be anywhere in the subtree and do not need to be specific child types.
const ThemeContext = createContext({ mode: 'light', toggle: () => {} });
function ThemeProvider({ children }) {
const [mode, setMode] = useState('light');
const toggle = () =>
setMode((m) => (m === 'light' ? 'dark' : 'light'));
return (
<ThemeContext.Provider value={{ mode, toggle }}>
{children}
</ThemeContext.Provider>
);
}
// Custom hook keeps consumers clean
function useTheme() {
return useContext(ThemeContext);
}
// Deep consumer — no prop drilling needed
function ThemeToggleButton() {
const { mode, toggle } = useTheme();
return <button onClick={toggle}>Switch to {mode === 'light' ? 'dark' : 'light'}</button>;
}
// App wires it together
<ThemeProvider>
<App /> {/* ThemeToggleButton anywhere inside has access */}
</ThemeProvider>
- Pair every Provider with a custom hook (
useTheme) so consumers get a clean call-site API. - Split contexts by update frequency — a slowly-changing locale and a rapidly-changing modal state belong in separate contexts to avoid unnecessary re-renders.
- Contrasted with compound components: Provider Pattern is for app-wide or subtree-wide concerns; compound components are for self-contained component suites.
React Component Patterns
(continued)Typed Compound Components
- At an advanced level, the interesting problem is not wiring a parent and child through Context — it is making the wiring type-safe and resilient to consumers reordering, omitting, or wrapping children. Start by typing the context value precisely and refusing a default object that could silently mask a missing provider.
type SelectContextValue<T> = { value: T; onChange: (next: T) => void; }; function createSelectContext<T>() { return createContext<SelectContextValue<T> | null>(null); } - A generic context factory lets
Select<string>andSelect<number>both get full type inference onvalueandonChange, instead of widening everything toanythe way a single shared context would.const SelectContext = createSelectContext<string>(); function useSelectContext() { const ctx = useContext(SelectContext); if (!ctx) throw new Error('Select.Option must be used inside <Select>'); return ctx; // narrowed to SelectContextValue<string>, never null past this line } - Context-based implicit sharing is convenient but invisible — a consumer cannot tell from JSX alone which children are required versus optional.
React.Children.mapwithcloneElementis the explicit alternative: the parent injects props directly into each child it recognizes, so the contract is visible in the child's own prop types rather than hidden behind a context read.function Select({ children, value, onChange }: SelectProps) { const items = Children.map(children, (child) => { if (!isValidElement<OptionProps>(child)) return child; return cloneElement(child, { selected: child.props.value === value, onSelect: () => onChange(child.props.value), }); }); return <div role="listbox">{items}</div>; } - Trade-off:
cloneElementbreaks if a consumer wraps an option in their own<div>or fragment — the clone only reaches direct children, so nesting silently drops the injected props. Context has no such restriction; any descendant, however deeply wrapped, can calluseSelectContext(). Prefer Context for any compound API you expect consumers to wrap or rearrange; reservecloneElementfor sealed, internal component sets where you control every usage site. - Build a flexible API by making the parent tolerant of missing, reordered, or extra children — derive an index from
Children.toArrayinstead of relying on prop-based IDs, so consumers can omit an item conditionally without renumbering the rest.const orderedIds = Children.toArray(children) .filter(isValidElement<OptionProps>) .map((child) => child.props.value); // Safe even if a caller does {showCanada && <Select.Option value="ca" />} - Export a discriminated union of sub-component prop types from the same module as the parent, so
Select.Optionusage outside<Select>is rejected by both the runtime guard and the type checker before the code ever ships.
React Component Patterns
(continued)Generic Render Prop APIs
- The basic mechanic — passing a function as a prop — is table stakes; the advanced concern is making that function's signature generic so the same component works across unrelated data shapes without consumers casting or duplicating the component.
type FetcherProps<T> = { url: string; children: (state: { data: T | null; error: Error | null; loading: boolean }) => React.ReactNode; }; function Fetcher<T>({ url, children }: FetcherProps<T>) { const [state, setState] = useState<{ data: T | null; error: Error | null; loading: boolean }>( { data: null, error: null, loading: true } ); useEffect(() => { let cancelled = false; fetch(url) .then((r) => r.json()) .then((data: T) => !cancelled && setState({ data, error: null, loading: false })) .catch((error) => !cancelled && setState({ data: null, error, loading: false })); return () => { cancelled = true; }; }, [url]); return <>{children(state)}</>; } // T is inferred at the call site — no casting needed <Fetcher<User[]> url="/api/users"> {({ data, loading }) => (loading ? <Spinner /> : <UserList users={data ?? []} />)} </Fetcher> - The modern hooks-based alternative to this exact component is a generic
useFetcher<T>(url)hook that returns the same{ data, error, loading }shape — for a component-tree consumer that already has hooks available, the hook is simpler because it adds no nesting and no extra component to the React tree.function useFetcher<T>(url: string) { const [state, setState] = useState<{ data: T | null; error: Error | null; loading: boolean }>( { data: null, error: null, loading: true } ); useEffect(() => { let cancelled = false; fetch(url).then((r) => r.json()).then((data: T) => !cancelled && setState({ data, error: null, loading: false })); return () => { cancelled = true; }; }, [url]); return state; } - Render props are still the better choice in three cases the hook cannot cover: class component consumers (which cannot call hooks at all), library code that must support both hook and non-hook consumers from one component, and JSX-driven APIs (like a
<Form>validation summary) where the producer needs to inject markup conditionally rather than just hand back values for the caller to render itself. - Watch for the classic render-prop performance trap: defining the render function inline causes React to see a new function reference every parent render, which defeats
React.memoon the render-prop component itself (though it does not affect the children the function returns).// Bad inside a frequently re-rendering parent: new closure every render <MemoizedFetcher<User[]> url={url}> {(state) => <UserList {...state} />} </MemoizedFetcher> // Better: stable reference via useCallback when the producer is memoized const renderUsers = useCallback( (state: FetchState<User[]>) => <UserList {...state} />, [] ); <MemoizedFetcher<User[]> url={url}>{renderUsers}</MemoizedFetcher> - This only matters when the render-prop component itself is wrapped in
React.memo— for an unmemoized producer, the inline closure costs nothing extra since the producer re-renders with its parent regardless.
React Component Patterns
(continued)Container/Presentational Pattern
The Container/Presentational pattern separates data-fetching logic from pure rendering. The container handles side effects, state, and API calls; the presentational component is a pure function of props with no side effects, making it trivially unit-testable and reusable across different data sources.
// Container: owns data fetching and state
function UserListContainer() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/users')
.then((r) => r.json())
.then((data) => { setUsers(data); setLoading(false); });
}, []);
if (loading) return <p>Loading...</p>;
return <UserList users={users} />;
}
// Presentational: pure UI, no side effects
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
- Container components are responsible for what data is shown; presentational components control how it looks.
- Presentational components are easy to test with hardcoded prop data — no mocking required.
- In a hooks world, the container is often a custom hook (
useUserList) rather than a wrapper component, but the separation of concerns stays the same. - Useful for Storybook: presentational components can be developed and documented with mock props independent of any API.
React Component Patterns
(FAQ)FAQ
A controlled component accepts its current value and an onChange callback as props, giving the parent full ownership of state. This makes the component predictable and testable — the parent can validate, transform, or synchronize the value before passing it back. It matters for API design because it establishes a clear contract: the component renders what it receives and reports changes upward; it never holds private state that can drift out of sync.
Both use React Context internally, but they serve different purposes. The Provider Pattern is about broadcasting shared state (like theme or auth) to any descendant in a subtree — consumers can be deeply nested and unrelated. Compound components are a tightly scoped API where a specific set of child components (like Tabs.Tab and Tabs.Panel) cooperate with a parent through a private context. Use Provider for app-wide concerns; use compound components for self-contained UI kits.
Use render props when the consumer needs to control the rendered output for each state a component produces, like loading, error, and success markup. A custom hook only returns values and functions — it cannot inject JSX on the producer's behalf. If consumers only need data, not markup control, a custom hook is simpler and avoids the extra nesting.
The shared context defaults to null, and the access hook throws an error if it reads null, which happens whenever a sub-component renders outside its parent's provider. Typing the context as Context<T | null> in TypeScript means this misuse is also caught at compile time, not just at runtime.
Because a presentational component is a pure function of props, you can render it directly in a test with whatever props you need — no mocks, no providers, no network calls. Pass a hardcoded array of user objects and assert on the rendered output. The container (or hook) that fetches real data is tested separately with mocked fetch or an msw handler, keeping the two concerns cleanly separated.
React Context Performance
Optimize context usage to avoid unnecessary re-renders with splitting and memoization.
TL;DR
- 01Split contexts into separate pieces to avoid cascading re-renders.
- 02Memoize context values to prevent unnecessary updates.
- 03Use useCallback for stable function references in context.
Tips
- 01Use useReducer instead of useState in context providers — dispatch is always stable, so no useCallback needed.
Warnings
- 01Over-optimization with memoization adds complexity — only optimize contexts that are accessed by many components and update frequently.
React Context Performance
(continued)Context Re-render Problem
Understand that every context consumer re-renders when any value in the context changes.
const AppContext = createContext<{ user: User | null; theme: string } | undefined>(undefined); function AppProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [theme, setTheme] = useState("light"); // All consumers re-render if EITHER user or theme changes return ( <AppContext.Provider value={{ user, theme, setUser, setTheme }}> {children} </AppContext.Provider> ); }Recognize that inline object literals create a new reference on every render, triggering all consumers.
// Bad: new object on every parent render — all consumers re-render unnecessarily <MyContext.Provider value={{ user, setUser }}> {children} </MyContext.Provider> // Good: stable reference when user hasn't changed const value = useMemo(() => ({ user, setUser }), [user]); <MyContext.Provider value={value}>{children}</MyContext.Provider>Profile with React DevTools before optimizing to confirm which components actually re-render.
// Open React DevTools > Profiler tab // Record an interaction, then inspect the flame chart // Only optimize components that show unnecessary rendersSpot the problem by logging renders inside a context consumer.
function ThemeButton() { const { theme } = useContext(AppContext)!; console.log("ThemeButton rendered"); // fires on every user change too return <button className={theme}>Click me</button>; }Understand that splitting contexts into smaller pieces is the primary fix for this problem.
// Instead of one big AppContext, use focused per-domain contexts: // UserContext → only user data // ThemeContext → only theme data // Each consumer subscribes only to what it needs
React Context Performance
(continued)Splitting Contexts
Create separate contexts for independent pieces of state so updates are isolated.
const UserContext = createContext<{ user: User | null; setUser: (u: User | null) => void } | undefined>(undefined); const ThemeContext = createContext<{ theme: string; setTheme: (t: string) => void } | undefined>(undefined); function AppProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const [theme, setTheme] = useState("light"); return ( <UserContext.Provider value={{ user, setUser }}> <ThemeContext.Provider value={{ theme, setTheme }}> {children} </ThemeContext.Provider> </UserContext.Provider> ); }Confirm that user-only consumers no longer re-render on theme changes after the split.
function UserBadge() { const { user } = useContext(UserContext)!; // Only re-renders when user changes — theme updates are ignored return <span>{user?.name}</span>; }Let each context update independently so unrelated parts of the tree stay stable.
function ThemeToggle() { const { theme, setTheme } = useContext(ThemeContext)!; // Changing theme does NOT re-render UserBadge above return <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>{theme}</button>; }Split read and write into separate contexts so read-only consumers never re-render from setter changes.
const UserStateContext = createContext<User | null>(null); // just the value const UserDispatchContext = createContext<(u: User | null) => void>(() => {}); // just the setter // Components that only display user data never re-render when setUser reference changes function UserDisplay() { const user = useContext(UserStateContext); return <p>{user?.name}</p>; }Subscribe only to the contexts a component needs by consuming each context separately.
function AdminPanel() { const { user } = useContext(UserContext)!; // user updates trigger this // ThemeContext NOT consumed here — theme changes are irrelevant return user?.role === "admin" ? <div>Admin tools</div> : null; }
React Context Performance
(continued)Memoizing Context Values
Wrap the context value object in
useMemoso a new reference is only created when dependencies change.function UserProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const userValue = useMemo( () => ({ user, setUser }), [user] // only creates a new object when user changes ); return ( <UserContext.Provider value={userValue}> {children} </UserContext.Provider> ); }List every value used from the context object in the dependency array to avoid stale closures.
const value = useMemo( () => ({ user, settings, updateUser }), [user, settings, updateUser] // include all three dependencies );Combine
useMemowith stable function references so functions don't invalidate the memo.const updateUser = useCallback((newUser: User) => { setUser(newUser); }, []); // stable reference — empty deps because setUser is always stable const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);Verify that memoization is working by checking that consumers don't re-render on unrelated parent renders.
// Parent re-renders due to its own state, but context value stays the same // Consumers should NOT re-render — confirm in React DevTools Profiler function ParentWithOwnState() { const [tick, setTick] = useState(0); // this state is unrelated to context return <button onClick={() => setTick(t => t + 1)}>Tick: {tick}</button>; }Avoid memoizing the initial value inline — always use
useMemoinside the provider component.// Wrong: initial value object is only created once but is a constant, not reactive <MyContext.Provider value={{ user: null, setUser: () => {} }}> // Right: useMemo inside the provider component reacts to state changes const value = useMemo(() => ({ user, setUser }), [user]); <MyContext.Provider value={value}>
React Context Performance
(continued)Memoized Consumers
Wrap context consumer components in
React.memoso they skip re-renders when props are unchanged.const UserDisplay = React.memo(function UserDisplay() { const { user } = useContext(UserContext)!; // Skips re-renders when parent state changes but context value is stable return <div>{user?.name}</div>; });Combine
React.memowith a memoized context value for the best re-render reduction.// Provider memoizes the value object const value = useMemo(() => ({ user, setUser }), [user]); // Consumer is wrapped in React.memo const UserBadge = React.memo(() => { const { user } = useContext(UserContext)!; return <span>{user?.name}</span>; }); // Only re-renders when the user object actually changesUnderstand that
React.memostill re-renders when the context value itself changes.// React.memo protects against prop changes, not context changes // If useContext returns a new reference, the component WILL re-render // Combine with useMemo on the provider value to control thisPass a custom comparison function to
React.memofor fine-grained prop-level control.const UserDisplay = React.memo( ({ label }: { label: string }) => { const { user } = useContext(UserContext)!; return <p>{label}: {user?.name}</p>; }, (prevProps, nextProps) => prevProps.label === nextProps.label // only re-render if label changes );Extract the context-consuming logic into a child component so
React.memocan protect the expensive UI.// Expensive chart component is memoized; only re-renders when chartData changes const Chart = React.memo(({ chartData }: { chartData: number[] }) => { return <canvas>{/* expensive render */}</canvas>; }); function ChartContainer() { const { chartData } = useContext(DashboardContext)!; return <Chart chartData={chartData} />; // passes stable reference when memoized }
React Context Performance
(continued)Callback Optimization
Wrap context callbacks in
useCallbackso their references stay stable across parent renders.function UserProvider({ children }: { children: React.ReactNode }) { const [user, setUser] = useState<User | null>(null); const updateUser = useCallback((newUser: User) => { setUser(newUser); // setUser is stable, so deps array is empty }, []); const value = useMemo(() => ({ user, updateUser }), [user, updateUser]); return ( <UserContext.Provider value={value}>{children}</UserContext.Provider> ); }Understand that functions created inline on every render invalidate the memoized context object.
// Bad: inline function creates new reference every render const value = useMemo(() => ({ user, updateUser: (u: User) => setUser(u), // new reference each render }), [user]); // updateUser changes every render, memo is useless // Good: useCallback gives updateUser a stable reference const updateUser = useCallback((u: User) => setUser(u), []); const value = useMemo(() => ({ user, updateUser }), [user, updateUser]);Use
useReducerdispatch instead of multiple callbacks — dispatch is always stable, nouseCallbackneeded.const [state, dispatch] = useReducer(userReducer, initialState); // dispatch never changes reference — safe to include in memoized context value const value = useMemo(() => ({ state, dispatch }), [state]);Compose multiple stable callbacks into a single memoized actions object for cleaner context values.
const actions = useMemo(() => ({ login: (credentials: Credentials) => dispatch({ type: "LOGIN", payload: credentials }), logout: () => dispatch({ type: "LOGOUT" }), updateProfile: (data: Partial<User>) => dispatch({ type: "UPDATE", payload: data }), }), [dispatch]); // dispatch is stable, so actions object is stablePrefer
useReduceroveruseStatein context providers to eliminate mostuseCallbackneeds.// useState approach: need useCallback for every mutator const [user, setUser] = useState(null); const updateName = useCallback((name) => setUser(u => ({ ...u!, name })), []); // useReducer approach: dispatch is always stable — no useCallback needed const [state, dispatch] = useReducer(reducer, { user: null });
React Context Performance
(FAQ)FAQ
Every component subscribed to a context re-renders whenever any value in that context changes, even if the component only uses an unrelated field. Split your context into separate providers per concern so updates to one domain don't trigger consumers of another.
Wrap the value object in useMemo with its dependencies: const value = useMemo(() => ({ user, setUser }), [user]). Without this, a new object reference is created on every parent render, causing all consumers to re-render even when the data hasn't changed.
Wrap a consumer in React.memo when it receives context values as props passed down from a parent rather than via useContext directly — this lets React bail out of re-rendering if the specific props it receives haven't changed. Components using useContext directly still re-render on context changes regardless of React.memo.
Functions defined inline in a component recreate on every render, making them new references that invalidate the memoized context object. Wrap each function with useCallback and declare its dependencies so the reference stays stable between renders.
There's no hard limit — providers are cheap and multiple providers composed in a tree is the recommended pattern. Combining unrelated state into one context trades boilerplate reduction for broader re-render scope, which is usually the wrong tradeoff for frequently-updating values.
React Memo and Lazy Loading
Optimize component rendering with React.memo, lazy loading, and code splitting strategies.
TL;DR
- 01Use React.memo to skip re-renders when props haven't changed.
- 02Use lazy loading to split code and load components on demand.
- 03Combine both for significant performance improvements.
Tips
- 01Unlike regular Suspense data-fetching failures, a failed dynamic import() throws a module load error — always pair React.lazy with an ErrorBoundary in production.
Warnings
- 01React.memo only does shallow comparison, so passing new objects or arrays as props defeats memoization — use useMemo or useCallback to keep props stable.
React Memo and Lazy Loading
(continued)React.memo
- Memoize a component to skip re-renders when props are the same.
const UserCard = React.memo(({ user }) => ( <div>{user.name}</div> )); - React.memo compares new and old props using shallow equality.
- Only skip re-renders if props haven't changed.
// Parent re-renders but UserCard doesn't if user prop is the same const Parent = () => { const [count, setCount] = useState(0); const user = useMemo(() => ({ id: 1, name: "Alice" }), []); return ( <> <UserCard user={user} /> <button onClick={() => setCount(count + 1)}> Count: {count} </button> </> ); }; - Use custom comparison for complex props.
const UserCard = React.memo( ({ user }) => <div>{user.name}</div>, (prevProps, nextProps) => { return prevProps.user.id === nextProps.user.id; } ); - Memoization has overhead, so only use when re-renders are expensive.
React Memo and Lazy Loading
(continued)Lazy Loading
Use
React.lazyto split code and load components on demand.import { lazy, Suspense } from "react"; const HeavyComponent = lazy(() => import("./HeavyComponent")); export default function App() { return ( <Suspense fallback={<p>Loading...</p>}> <HeavyComponent /> </Suspense> ); }The component code is not included in the main bundle — it fetches only when first rendered.
Wrap lazy components in an
ErrorBoundaryto handle chunk load failures gracefully.<ErrorBoundary fallback={<p>Failed to load — check your connection.</p>}> <Suspense fallback={<p>Loading...</p>}> <HeavyComponent /> </Suspense> </ErrorBoundary>Load conditional UI only when needed — modals, settings panels, and charts are ideal candidates.
const SettingsPanel = lazy(() => import("./SettingsPanel")); {showSettings && ( <Suspense fallback={<p>Loading settings...</p>}> <SettingsPanel /> </Suspense> )}Works great for route-based code splitting in React Router or any SPA router.
React Memo and Lazy Loading
(continued)Route-Based Code Splitting
- Split code by route to load pages on demand in Next.js or React Router.
import { lazy } from "react"; const Dashboard = lazy(() => import("./pages/Dashboard")); const Settings = lazy(() => import("./pages/Settings")); const Profile = lazy(() => import("./pages/Profile")); - Each route is a separate chunk loaded when the user navigates.
- Reduces the initial bundle size significantly.
<Routes> <Route path="/dashboard" element={<Dashboard />} /> <Route path="/settings" element={<Settings />} /> <Route path="/profile" element={<Profile />} /> </Routes> - Most effective optimization for large applications.
- Combine with Suspense for loading states while pages load.
React Memo and Lazy Loading
(continued)Component Splitting
- Split large components into smaller memoized pieces.
const Header = React.memo(() => <header>...</header>); const Content = React.memo(({ data }) => <main>{data}</main>); const Footer = React.memo(() => <footer>...</footer>); export default function Page({ data }) { return ( <> <Header /> <Content data={data} /> <Footer /> </> ); } - Memoized children don't re-render when parent re-renders.
- Only effective if children receive stable props.
- Combine with useCallback to keep function props stable.
const handleClick = useCallback(() => { // Handle click }, []); <Button onClick={handleClick} />
React Memo and Lazy Loading
(continued)Performance Monitoring
- Use Profiler API to measure component rendering time.
import { Profiler } from "react"; <Profiler id="dashboard" onRender={onRender}> <Dashboard /> </Profiler> function onRender(id, phase, actualDuration) { console.log(`${id} (${phase}) took ${actualDuration}ms`); } - Use React DevTools Profiler to identify slow components.
- Measure before and after optimization to confirm improvements.
- Only optimize the slow components, not everything.
// Profile to find slow components first // Then apply memo, lazy, or splitting strategically - Avoid premature optimization — measure real performance first.
React Memo and Lazy Loading
(FAQ)FAQ
Use React.memo to prevent a component from re-rendering when its props haven't changed. Use useMemo inside a component to memoize expensive computed values. They solve different problems and are often used together.
Wrap your import with React.lazy(() => import('./MyComponent')) and render it inside a Suspense boundary with a fallback UI. The component's code is only fetched when it's first rendered.
No — children is a new object reference on every render, so React.memo will always re-render if you pass JSX as children. Lift the children out or restructure your component tree to avoid this.
Route-based splitting lazy-loads entire page-level components when a route is first visited, which is the highest-impact change for initial load time. Component splitting targets large UI pieces (modals, charts) that aren't visible on first paint.
Use the Coverage tab in Chrome DevTools to see how much JavaScript is unused on initial load, and check the Network tab to confirm chunks are loaded on demand. React DevTools Profiler shows which components re-render unnecessarily.