1. React Components and JSX Syntax
1.1 Functional Components and Arrow Function Syntax
Pattern
Syntax
Description
Use Case
Function Declaration
function Comp() {}
Traditional function syntax for components
Named exports, hoisting needed
Arrow Function
const Comp = () => {}
Modern ES6 arrow function syntax
Preferred for most components
Implicit Return
const Comp = () => (<div/>)
Single expression return without braces
Simple, presentational components
Named Export
export function Comp() {}
Export component directly
Multiple exports per file
Default Export
export default Comp
Single default export per file
One main component per file
Example: Component declaration patterns
// Function declaration
function Welcome () {
return < h1 >Hello</ h1 >;
}
// Arrow function with explicit return
const Greeting = () => {
return < h1 >Hi there</ h1 >;
};
// Arrow function with implicit return
const SimpleComp = () => (< div >Simple</ div >);
// With props destructuring
const User = ({ name , age }) => (
< div >{name}, {age}</ div >
);
1.2 JSX Elements and Expression Embedding
Feature
Syntax
Description
Example
JSX Element
<div>content</div>
HTML-like syntax in JavaScript
<h1>Title</h1>
Self-closing Tag
<Component />
Elements without children must self-close
<img src="..." />
Expression Embedding
{expression}
Execute JavaScript inside JSX
{2 + 2}
Attribute Expression
attr={value}
Dynamic attribute values
className={style}
String Literal
attr="value"
Static string attributes
type="text"
CamelCase Props
className, onClick
HTML attributes use camelCase in JSX
tabIndex, htmlFor
Comments in JSX
{/* comment */}
JavaScript-style comments in curly braces
{/* Note */}
Example: JSX expressions and embedding
const UserCard = ({ user , isOnline }) => {
const greeting = 'Welcome' ;
return (
< div className = "card" >
{ /* Dynamic content */ }
< h2 >{greeting}, {user.name}!</ h2 >
{ /* Expressions */ }
< p >Age: {user.age * 2 }</ p >
{ /* Method calls */ }
< p >{user.email. toLowerCase ()}</ p >
{ /* Dynamic attributes */ }
< img src = {user.avatar} alt = {user.name} />
< span className = {isOnline ? 'active' : 'inactive' }>
{isOnline ? 'Online' : 'Offline' }
</ span >
</ div >
);
};
Note: JSX is syntactic sugar for React.createElement(). All JSX is transformed to
JavaScript function calls during compilation.
1.3 Component Props and Destructuring Patterns
Pattern
Syntax
Description
Use Case
Props Object
function Comp(props) {}
Access props via object
When passing all props down
Destructuring
({ name, age }) => {}
Extract props directly in parameters
Most common pattern
Default Values
({ name = 'Guest' }) => {}
Provide fallback values
Optional props with defaults
Rest Props
({ name, ...rest }) => {}
Collect remaining props
Forwarding props to children
Nested Destructuring
({ user: { name } }) => {}
Extract nested object properties
Complex prop structures
Prop Spreading
<Comp {...props} />
Pass all props to child
Wrapper components
Example: Props destructuring patterns
// Basic destructuring with defaults
const Button = ({
text = 'Click' ,
onClick,
disabled = false
}) => (
< button onClick = {onClick} disabled = {disabled}>
{text}
</ button >
);
// Rest props and spreading
const Input = ({ label , ... inputProps }) => (
< div >
< label >{label}</ label >
< input { ... inputProps} />
</ div >
);
// Nested destructuring
const UserProfile = ({
user: { name, email, address: { city } }
}) => (
< div >
< h2 >{name}</ h2 >
< p >{email}</ p >
< p >City: {city}</ p >
</ div >
);
// Usage with explicit props
< Input label = "Email" type = "email" placeholder = "Enter email" />
1.4 Children Prop and Component Composition
Concept
Syntax
Description
Use Case
Children Prop
props.children
Content passed between component tags
Wrapper/container components
Children Destructuring
({ children }) => {}
Extract children directly
Cleaner component code
Multiple Children
<Comp>{child1}{child2}</Comp>
Pass multiple elements as children
Complex layouts
Render Props
children(data)
Children as function pattern
Share logic/data with children
Named Slots
header, footer props
Multiple named content areas
Flexible layouts
Composition
<A><B><C /></B></A>
Nest components for hierarchy
Building complex UIs
Example: Children and composition patterns
// Simple children wrapper
const Card = ({ children , title }) => (
< div className = "card" >
< h3 >{title}</ h3 >
< div className = "card-body" >{children}</ div >
</ div >
);
// Render props pattern
const DataProvider = ({ children , data }) => (
< div >{ children (data)}</ div >
);
// Usage
< DataProvider data = {users}>
{( users ) => (
< ul >
{users. map ( u => < li key = {u.id}>{u.name}</ li >)}
</ ul >
)}
</ DataProvider >
// Named slots pattern
const Layout = ({ header , children , footer }) => (
< div >
< header >{header}</ header >
< main >{children}</ main >
< footer >{footer}</ footer >
</ div >
);
// Composition
< Layout
header = {< Nav />}
footer = {< Footer />}
>
< Card title = "Welcome" >
< p >Content here</ p >
</ Card >
</ Layout >
1.5 JSX Conditional Rendering Patterns
Pattern
Syntax
Description
Use Case
Ternary Operator
{condition ? <A/> : <B/>}
Inline if-else rendering
Two-way conditional rendering
Logical AND (&&)
{condition && <Component/>}
Render if condition is truthy
Optional content display
Logical OR (||)
{value || 'default'}
Fallback value if falsy
Default content
Nullish Coalescing
{value ?? 'default'}
Fallback only for null/undefined
Preserve falsy values (0, '')
Early Return
if (!data) return null
Guard clause before JSX
Loading/error states
IIFE Pattern
{(() => {})()}
Complex logic in JSX
Multi-statement conditionals
Element Variables
let el; if (...) el = ...
Store JSX in variables
Complex conditional logic
Example: Conditional rendering techniques
const UserStatus = ({ user , isLoading , error }) => {
// Early return for loading
if (isLoading) return < Spinner />;
// Early return for error
if (error) return < Error message = {error} />;
// Early return for no data
if ( ! user) return null ;
return (
< div >
{ /* Ternary operator */ }
{user.isPremium ? (
< PremiumBadge />
) : (
< FreeBadge />
)}
{ /* Logical AND */ }
{user.notifications && (
< NotificationBadge count = {user.notifications} />
)}
{ /* Nullish coalescing */ }
< p >Score: {user.score ?? 0 }</ p >
{ /* Multiple conditions with element variable */ }
{(() => {
if (user.role === 'admin' ) return < AdminPanel />;
if (user.role === 'moderator' ) return < ModPanel />;
return < UserPanel />;
})()}
</ div >
);
};
// Element variable pattern
const StatusDisplay = ({ status }) => {
let statusElement;
switch (status) {
case 'success' :
statusElement = < SuccessIcon />;
break ;
case 'error' :
statusElement = < ErrorIcon />;
break ;
default :
statusElement = < InfoIcon />;
}
return < div >{statusElement}</ div >;
};
Warning: Avoid using && with numbers like
{count && <Text/>} - if count is 0, it renders "0". Use
{count > 0 && <Text/>} or {!!count && <Text/>} instead.
1.6 JSX Fragment Syntax and React.Fragment
Syntax
Usage
Description
Use Case
Short Syntax
<>...</>
Empty tag fragment shorthand
Group elements without wrapper
Explicit Fragment
<React.Fragment>
Full fragment component
When key prop is needed
Fragment with Key
<Fragment key={id}>
Fragment in lists with key
Mapping multiple elements
Import Fragment
import { Fragment }
Named import from React
Cleaner than React.Fragment
Example: Fragment usage patterns
import { Fragment } from 'react' ;
// Short syntax - most common
const UserInfo = () => (
<>
< h1 >User Profile</ h1 >
< p >Details</ p >
</>
);
// With key prop (required in lists)
const GlossaryList = ({ items }) => (
< dl >
{items. map ( item => (
< Fragment key = {item.id}>
< dt >{item.term}</ dt >
< dd >{item.description}</ dd >
</ Fragment >
))}
</ dl >
);
// Avoiding unnecessary divs
const Table = () => (
< table >
< tbody >
{data. map ( row => (
<>
< tr key = {row.id}>
< td >{row.name}</ td >
</ tr >
{row.details && (
< tr >
< td colSpan = "2" >{row.details}</ td >
</ tr >
)}
</>
))}
</ tbody >
</ table >
);
// Conditional multiple elements
const Form = ({ showHelp }) => (
< form >
< input type = "text" />
{showHelp && (
<>
< p >Help text</ p >
< button >Learn More</ button >
</>
)}
</ form >
);
Note: Fragments don't create extra DOM nodes, improving performance and maintaining semantic
HTML. Use <></> when no props needed, <Fragment key={...}> when mapping with keys.
2. React Hooks Fundamentals
2.1 useState Hook and State Management
Pattern
Syntax
Description
Use Case
Basic useState
const [state, setState] = useState(initial)
Declare state variable with setter
Simple state values
Lazy Initialization
useState(() => expensive())
Initialize state with function
Expensive initial computation
Functional Update
setState(prev => prev + 1)
Update based on previous state
Batched updates, async operations
Multiple States
useState() for each value
Split unrelated state into separate hooks
Independent state management
Object State
useState({ key: value })
Store related data together
Form data, related properties
Array State
useState([])
Manage lists and collections
Todo lists, dynamic items
Example: useState patterns and updates
import { useState } from 'react' ;
// Basic counter
const Counter = () => {
const [ count , setCount ] = useState ( 0 );
return (
< div >
< p >Count: {count}</ p >
< button onClick = {() => setCount (count + 1 )}>+</ button >
{ /* Functional update - safer for async */ }
< button onClick = {() => setCount ( prev => prev + 1 )}>+</ button >
</ div >
);
};
// Lazy initialization
const ExpensiveComponent = () => {
const [ data , setData ] = useState (() => {
return computeExpensiveValue ();
});
return < div >{data}</ div >;
};
// Object state with spreading
const Form = () => {
const [ form , setForm ] = useState ({ name: '' , email: '' });
const updateField = ( field , value ) => {
setForm ( prev => ({ ... prev, [field]: value }));
};
return (
< form >
< input
value = {form.name}
onChange = {( e ) => updateField ( 'name' , e.target.value)}
/>
< input
value = {form.email}
onChange = {( e ) => updateField ( 'email' , e.target.value)}
/>
</ form >
);
};
// Array state operations
const TodoList = () => {
const [ todos , setTodos ] = useState ([]);
const addTodo = ( text ) => {
setTodos ( prev => [ ... prev, { id: Date. now (), text }]);
};
const removeTodo = ( id ) => {
setTodos ( prev => prev. filter ( t => t.id !== id));
};
const updateTodo = ( id , newText ) => {
setTodos ( prev => prev. map ( t =>
t.id === id ? { ... t, text: newText } : t
));
};
return < ul >{ /* render todos */ }</ ul >;
};
Warning: State updates are asynchronous and batched. Always use functional updates
setState(prev => ...) when new state depends on previous state.
2.2 useEffect Hook and Side Effects
Pattern
Syntax
Description
Use Case
Basic Effect
useEffect(() => {})
Runs after every render
DOM updates, logging (avoid)
Effect with Dependencies
useEffect(() => {}, [deps])
Runs when dependencies change
Data fetching, subscriptions
Mount Only Effect
useEffect(() => {}, [])
Runs once on mount
Initial setup, API calls
Cleanup Function
useEffect(() => { return () => {} })
Cleanup before unmount/re-run
Unsubscribe, clear timers
Multiple Effects
useEffect() for each concern
Separate unrelated side effects
Better code organization
Conditional Effect
if (condition) { /* effect */ }
Execute effect conditionally inside
Conditional subscriptions
Example: useEffect patterns and cleanup
import { useState, useEffect } from 'react' ;
// Data fetching with cleanup
const UserProfile = ({ userId }) => {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
let isMounted = true ; // Cleanup flag
const fetchUser = async () => {
setLoading ( true );
try {
const response = await fetch (\ `/api/users/ \$ {userId} \` );
const data = await response.json();
if (isMounted) setUser(data);
} catch (error) {
if (isMounted) console.error(error);
} finally {
if (isMounted) setLoading(false);
}
};
fetchUser();
return () => {
isMounted = false; // Prevent state update after unmount
};
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
};
// Timer with cleanup
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
return () => clearInterval(interval); // Cleanup
}, []); // Empty deps - mount only
return <div>{seconds}s</div>;
};
// Event listener with cleanup
const WindowSize = () => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []); // No dependencies
return <div>{size.width} x {size.height}</div>;
};
// Multiple effects for separation of concerns
const ChatRoom = ({ roomId }) => {
const [messages, setMessages] = useState([]);
// Effect 1: Connect to chat
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
// Effect 2: Load message history
useEffect(() => {
loadMessages(roomId).then(setMessages);
}, [roomId]);
// Effect 3: Update document title
useEffect(() => {
document.title = \` Chat: \$ {roomId} \` ;
return () => { document.title = 'App'; };
}, [roomId]);
return <div>{/* UI */}</div>;
};
Note: In React 18+, effects run twice in development (Strict Mode) to help catch bugs. Always
implement cleanup functions properly.
2.3 useContext Hook for Context Consumption
Concept
Syntax
Description
Use Case
useContext Hook
const value = useContext(Context)
Access context value in component
Consume shared state
Context Creation
createContext(defaultValue)
Create context object
Define context for provider
Provider Component
<Context.Provider value={...}>
Provide context value to children
Share data down tree
Multiple Contexts
useContext(Context1), useContext(Context2)
Consume multiple contexts
Separate concerns (theme, auth)
Default Value
createContext(fallback)
Value when no provider exists
Testing, development defaults
Example: Context creation and consumption
import { createContext, useContext, useState } from 'react' ;
// 1. Create context
const ThemeContext = createContext ( 'light' );
// 2. Create provider component
const ThemeProvider = ({ children }) => {
const [ theme , setTheme ] = useState ( 'light' );
const toggleTheme = () => {
setTheme ( prev => prev === 'light' ? 'dark' : 'light' );
};
return (
< ThemeContext.Provider value = {{ theme, toggleTheme }}>
{children}
</ ThemeContext.Provider >
);
};
// 3. Create custom hook for easier consumption
const useTheme = () => {
const context = useContext (ThemeContext);
if ( ! context) {
throw new Error ( 'useTheme must be used within ThemeProvider' );
}
return context;
};
// 4. Consume context in components
const Header = () => {
const { theme , toggleTheme } = useTheme ();
return (
< header className = {theme}>
< button onClick = {toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light' }
</ button >
</ header >
);
};
const Content = () => {
const { theme } = useTheme ();
return < div className = {theme}>Content</ div >;
};
// 5. App with provider
const App = () => (
< ThemeProvider >
< Header />
< Content />
</ ThemeProvider >
);
// Multiple contexts example
const AuthContext = createContext ( null );
const SettingsContext = createContext ({});
const Dashboard = () => {
const auth = useContext (AuthContext);
const settings = useContext (SettingsContext);
const theme = useContext (ThemeContext);
return < div >{auth.user.name}</ div >;
};
2.4 useReducer Hook for Complex State Logic
Pattern
Syntax
Description
Use Case
Basic useReducer
useReducer(reducer, initialState)
Alternative to useState for complex state
Multiple related state values
Reducer Function
(state, action) => newState
Pure function handling state updates
Centralized state logic
Dispatch Action
dispatch({ type, payload })
Trigger state update
User interactions, events
Lazy Initialization
useReducer(reducer, arg, init)
Compute initial state lazily
Expensive initialization
Action Types
const ACTIONS = { ADD, UPDATE }
Constants for action types
Type safety, autocomplete
Immer Integration
produce(state, draft => {})
Mutable-style updates with Immer
Complex nested updates
Example: useReducer for complex state management
import { useReducer } from 'react' ;
// Action types
const ACTIONS = {
ADD_TODO: 'add_todo' ,
TOGGLE_TODO: 'toggle_todo' ,
DELETE_TODO: 'delete_todo' ,
SET_FILTER: 'set_filter'
};
// Reducer function
const todoReducer = ( state , action ) => {
switch (action.type) {
case ACTIONS . ADD_TODO :
return {
... state,
todos: [ ... state.todos, {
id: Date. now (),
text: action.payload,
completed: false
}]
};
case ACTIONS . TOGGLE_TODO :
return {
... state,
todos: state.todos. map ( todo =>
todo.id === action.payload
? { ... todo, completed: ! todo.completed }
: todo
)
};
case ACTIONS . DELETE_TODO :
return {
... state,
todos: state.todos. filter ( t => t.id !== action.payload)
};
case ACTIONS . SET_FILTER :
return { ... state, filter: action.payload };
default :
return state;
}
};
// Component using useReducer
const TodoApp = () => {
const initialState = {
todos: [],
filter: 'all'
};
const [ state , dispatch ] = useReducer (todoReducer, initialState);
const addTodo = ( text ) => {
dispatch ({ type: ACTIONS . ADD_TODO , payload: text });
};
const toggleTodo = ( id ) => {
dispatch ({ type: ACTIONS . TOGGLE_TODO , payload: id });
};
const deleteTodo = ( id ) => {
dispatch ({ type: ACTIONS . DELETE_TODO , payload: id });
};
const filteredTodos = state.todos. filter ( todo => {
if (state.filter === 'active' ) return ! todo.completed;
if (state.filter === 'completed' ) return todo.completed;
return true ;
});
return (
< div >
< input onSubmit = {( e ) => {
addTodo (e.target.value);
e.target.value = '' ;
}} />
< ul >
{filteredTodos. map ( todo => (
< li key = {todo.id}>
< input
type = "checkbox"
checked = {todo.completed}
onChange = {() => toggleTodo (todo.id)}
/>
{todo.text}
< button onClick = {() => deleteTodo (todo.id)}>✕</ button >
</ li >
))}
</ ul >
< div >
{[ 'all' , 'active' , 'completed' ]. map ( filter => (
< button
key = {filter}
onClick = {() => dispatch ({
type: ACTIONS . SET_FILTER ,
payload: filter
})}
>
{filter}
</ button >
))}
</ div >
</ div >
);
};
Note: Use useReducer over useState when: (1) complex state logic, (2) multiple sub-values, (3) next state
depends on previous state, (4) need to optimize performance with deep updates.
2.5 useMemo and useCallback Optimization Hooks
Hook
Syntax
Description
Use Case
useMemo
useMemo(() => computation, [deps])
Memoize computed values
Expensive calculations
useCallback
useCallback(() => {}, [deps])
Memoize function references
Prevent child re-renders
Dependencies Array
[dep1, dep2]
Recompute when deps change
Control memoization lifecycle
Empty Dependencies
useMemo(() => val, [])
Compute once, never update
Constants, one-time calculations
Referential Equality
React.memo() with callbacks
Prevent unnecessary re-renders
Optimized child components
Example: useMemo and useCallback optimization
import { useMemo, useCallback, memo } from 'react' ;
// useMemo for expensive computations
const DataTable = ({ data , filterText }) => {
// Memoize filtered and sorted data
const processedData = useMemo (() => {
console. log ( 'Processing data...' ); // Only logs when deps change
return data
. filter ( item => item.name. includes (filterText))
. sort (( a , b ) => a.name. localeCompare (b.name));
}, [data, filterText]);
return (
< table >
{processedData. map ( item => (
< tr key = {item.id}>< td >{item.name}</ td ></ tr >
))}
</ table >
);
};
// useCallback to memoize functions
const ParentComponent = () => {
const [ count , setCount ] = useState ( 0 );
const [ text , setText ] = useState ( '' );
// Without useCallback - new function every render
const handleClick = () => {
console. log ( 'Clicked' );
};
// With useCallback - same function reference
const handleClickMemo = useCallback (() => {
console. log ( 'Clicked' , count);
}, [count]); // Recreate when count changes
return (
< div >
< input value = {text} onChange = {( e ) => setText (e.target.value)} />
{ /* Child re-renders when text changes without memo */ }
< ChildComponent onClick = {handleClickMemo} />
< button onClick = {() => setCount (count + 1 )}>
Count: {count}
</ button >
</ div >
);
};
// Memoized child component
const ChildComponent = memo (({ onClick }) => {
console. log ( 'Child rendered' );
return < button onClick = {onClick}>Click Me</ button >;
});
// Complex example combining both
const SearchableList = ({ items }) => {
const [ query , setQuery ] = useState ( '' );
const [ sortOrder , setSortOrder ] = useState ( 'asc' );
// Memoize filtered items
const filteredItems = useMemo (() => {
return items. filter ( item =>
item.name. toLowerCase (). includes (query. toLowerCase ())
);
}, [items, query]);
// Memoize sorted items
const sortedItems = useMemo (() => {
return [ ... filteredItems]. sort (( a , b ) => {
const compare = a.name. localeCompare (b.name);
return sortOrder === 'asc' ? compare : - compare;
});
}, [filteredItems, sortOrder]);
// Memoize callback
const handleSort = useCallback (() => {
setSortOrder ( prev => prev === 'asc' ? 'desc' : 'asc' );
}, []);
return (
< div >
< input
value = {query}
onChange = {( e ) => setQuery (e.target.value)}
/>
< button onClick = {handleSort}>Sort</ button >
< List items = {sortedItems} />
</ div >
);
};
Warning: Don't overuse memoization! Only use useMemo and useCallback
when: (1) expensive computations, (2) referential equality matters (props to memoized children), (3) profiling
shows performance issues.
2.6 useRef Hook for DOM and Value References
Use Case
Syntax
Description
Example
DOM Reference
const ref = useRef(null)
Access DOM element directly
<input ref={ref} />
Mutable Value
ref.current = value
Store value that doesn't trigger re-render
Previous state, timers
Access Element
ref.current.focus()
Call DOM methods imperatively
Focus, scroll, measure
Instance Variable
useRef(initialValue)
Persist value across renders
Intervals, subscriptions
Previous Value
useRef() + useEffect
Track previous prop/state value
Compare changes
Ref Callback
ref={(node) => {}}
Function called with DOM node
Dynamic refs, measurements
Example: useRef for DOM access and mutable values
import { useRef, useEffect } from 'react' ;
// Focus input on mount
const AutoFocusInput = () => {
const inputRef = useRef ( null );
useEffect (() => {
inputRef.current. focus ();
}, []);
return < input ref = {inputRef} />;
};
// Store timer ID without triggering re-renders
const Timer = () => {
const [ count , setCount ] = useState ( 0 );
const intervalRef = useRef ( null );
const start = () => {
if ( ! intervalRef.current) {
intervalRef.current = setInterval (() => {
setCount ( c => c + 1 );
}, 1000 );
}
};
const stop = () => {
if (intervalRef.current) {
clearInterval (intervalRef.current);
intervalRef.current = null ;
}
};
useEffect (() => {
return () => stop (); // Cleanup
}, []);
return (
< div >
< p >{count}</ p >
< button onClick = {start}>Start</ button >
< button onClick = {stop}>Stop</ button >
</ div >
);
};
// Track previous value
const usePrevious = ( value ) => {
const ref = useRef ();
useEffect (() => {
ref.current = value;
}, [value]);
return ref.current;
};
const Counter = ({ count }) => {
const prevCount = usePrevious (count);
return (
< div >
Current: {count}, Previous: {prevCount}
</ div >
);
};
// Measure element dimensions
const MeasureElement = () => {
const divRef = useRef ( null );
const [ dimensions , setDimensions ] = useState ({});
useEffect (() => {
if (divRef.current) {
const { width , height } = divRef.current. getBoundingClientRect ();
setDimensions ({ width, height });
}
}, []);
return (
< div ref = {divRef}>
Size: {dimensions.width} x {dimensions.height}
</ div >
);
};
// Ref callback for dynamic refs
const DynamicList = ({ items }) => {
const itemRefs = useRef ( new Map ());
const scrollToItem = ( id ) => {
const node = itemRefs.current. get (id);
node?. scrollIntoView ({ behavior: 'smooth' });
};
return (
< ul >
{items. map ( item => (
< li
key = {item.id}
ref = {( node ) => {
if (node) {
itemRefs.current. set (item.id, node);
} else {
itemRefs.current. delete (item.id);
}
}}
>
{item.name}
</ li >
))}
</ ul >
);
};
Note: useRef returns a mutable object whose
.current property is initialized with the passed argument. The object persists for the component's
lifetime. Changing .current doesn't cause re-renders.
3. Modern React 18+ Features REACT 18+
3.1 useTransition Hook for Non-blocking Updates NEW
Feature
Syntax
Description
Use Case
useTransition Hook
const [isPending, startTransition] = useTransition()
Mark state updates as non-urgent transitions
Keep UI responsive during updates
startTransition
startTransition(() => {})
Wrap state updates to mark as transition
Heavy computations, filtering
isPending Flag
isPending ? 'Loading' : 'Done'
Boolean indicating transition in progress
Show loading indicators
Priority System
Urgent vs transition updates
React prioritizes urgent updates first
Input responsiveness
Interruptible Renders
React can pause transitions
Allow urgent updates to interrupt
Smooth user experience
Example: useTransition for responsive filtering
import { useState, useTransition } from 'react' ;
const SearchableList = ({ items }) => {
const [ query , setQuery ] = useState ( '' );
const [ filteredItems , setFilteredItems ] = useState (items);
const [ isPending , startTransition ] = useTransition ();
const handleSearch = ( value ) => {
// Urgent: Update input immediately
setQuery (value);
// Non-urgent: Filter in background
startTransition (() => {
const filtered = items. filter ( item =>
item.name. toLowerCase (). includes (value. toLowerCase ())
);
setFilteredItems (filtered);
});
};
return (
< div >
< input
type = "text"
value = {query}
onChange = {( e ) => handleSearch (e.target.value)}
placeholder = "Search..."
/>
{isPending ? (
< div >Updating results...</ div >
) : (
< ul >
{filteredItems. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
)}
</ div >
);
};
// Tab switching example
const TabContainer = () => {
const [ activeTab , setActiveTab ] = useState ( 'tab1' );
const [ isPending , startTransition ] = useTransition ();
const switchTab = ( tab ) => {
startTransition (() => {
setActiveTab (tab);
});
};
return (
< div >
< div className = {isPending ? 'loading' : '' }>
< button onClick = {() => switchTab ( 'tab1' )}>Tab 1</ button >
< button onClick = {() => switchTab ( 'tab2' )}>Tab 2</ button >
< button onClick = {() => switchTab ( 'tab3' )}>Tab 3</ button >
</ div >
< TabContent tab = {activeTab} />
</ div >
);
};
Note: useTransition enables concurrent rendering. Transition
updates can be interrupted by more urgent updates, keeping the UI responsive even during expensive operations.
3.2 useDeferredValue Hook for Deferred Values NEW
Feature
Syntax
Description
Use Case
useDeferredValue
const deferred = useDeferredValue(value)
Defer updating a value to keep UI responsive
Expensive child component updates
Deferred Rendering
<Component value={deferred} />
Pass deferred value to expensive component
Debounce-like behavior
Initial Value
useDeferredValue(value, initialValue)
Optional initial value during SSR
Server-side rendering
Value Comparison
value !== deferred
Check if deferral is in progress
Show loading states
Example: useDeferredValue for responsive search
import { useState, useDeferredValue, memo } from 'react' ;
const SearchResults = ({ query }) => {
const deferredQuery = useDeferredValue (query);
const isStale = query !== deferredQuery;
return (
< div style = {{ opacity: isStale ? 0.5 : 1 }}>
< ExpensiveList query = {deferredQuery} />
</ div >
);
};
// Expensive component that benefits from deferral
const ExpensiveList = memo (({ query }) => {
const items = useMemo (() => {
// Expensive filtering/computation
return largeDataset. filter ( item =>
item.name. toLowerCase (). includes (query. toLowerCase ())
);
}, [query]);
return (
< ul >
{items. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
);
});
// Complete search component
const SearchPage = () => {
const [ query , setQuery ] = useState ( '' );
const deferredQuery = useDeferredValue (query);
const isSearching = query !== deferredQuery;
return (
< div >
< input
type = "text"
value = {query}
onChange = {( e ) => setQuery (e.target.value)}
placeholder = "Search..."
/>
{isSearching && < span >Searching...</ span >}
< SearchResults query = {deferredQuery} />
</ div >
);
};
// Comparison: useTransition vs useDeferredValue
// useTransition: You control the state update
// useDeferredValue: React controls when to update the value
Note: Use useDeferredValue when you can't control the state
update (props from parent). Use useTransition when you control the state update.
3.3 useId Hook for Unique Identifiers NEW
Feature
Syntax
Description
Use Case
useId Hook
const id = useId()
Generate unique ID for accessibility attributes
Form labels, ARIA attributes
SSR Safe
Same ID on client and server
Consistent IDs across hydration
Avoid hydration mismatches
Multiple IDs
id + '-label', id + '-error'
Derive multiple IDs from single hook
Related elements
List Items
Don't use for key prop
Keys should be from data, not generated
Use stable data IDs
import { useId } from 'react' ;
// Basic form field with unique ID
const TextField = ({ label , error }) => {
const id = useId ();
const errorId = id + '-error' ;
return (
< div >
< label htmlFor = {id}>{label}</ label >
< input
id = {id}
type = "text"
aria-describedby = {error ? errorId : undefined }
aria-invalid = {error ? 'true' : 'false' }
/>
{error && (
< span id = {errorId} role = "alert" >
{error}
</ span >
)}
</ div >
);
};
// Complex form with multiple fields
const ContactForm = () => {
const id = useId ();
return (
< form >
< div >
< label htmlFor = {id + '-name' }>Name</ label >
< input id = {id + '-name' } type = "text" />
</ div >
< div >
< label htmlFor = {id + '-email' }>Email</ label >
< input id = {id + '-email' } type = "email" />
</ div >
< div >
< label htmlFor = {id + '-message' }>Message</ label >
< textarea id = {id + '-message' } />
</ div >
</ form >
);
};
// Reusable input component
const Input = ({ label , type = 'text' , ... props }) => {
const id = useId ();
return (
<>
< label htmlFor = {id}>{label}</ label >
< input id = {id} type = {type} { ... props} />
</>
);
};
// ARIA example
const Accordion = ({ title , children }) => {
const [ isOpen , setIsOpen ] = useState ( false );
const id = useId ();
const headerId = id + '-header' ;
const panelId = id + '-panel' ;
return (
< div >
< button
id = {headerId}
aria-expanded = {isOpen}
aria-controls = {panelId}
onClick = {() => setIsOpen ( ! isOpen)}
>
{title}
</ button >
< div
id = {panelId}
role = "region"
aria-labelledby = {headerId}
hidden = { ! isOpen}
>
{children}
</ div >
</ div >
);
};
Warning: Don't use useId to generate keys in a list. Keys should come from your
data. Use useId only for accessibility attributes like id, htmlFor,
aria-*.
3.4 useSyncExternalStore Hook for External State NEW
Feature
Syntax
Description
Use Case
useSyncExternalStore
useSyncExternalStore(subscribe, getSnapshot)
Subscribe to external store
Redux, Zustand, browser APIs
Subscribe Function
subscribe(callback)
Subscribe to store changes
Return unsubscribe function
getSnapshot Function
getSnapshot()
Get current store value
Must return immutable value
SSR getSnapshot
useSyncExternalStore(..., getServerSnapshot)
Server-side initial value
Hydration consistency
Tearing Prevention
Consistent state across components
No visual inconsistencies
Concurrent rendering safety
Example: useSyncExternalStore for browser APIs
import { useSyncExternalStore } from 'react' ;
// Browser online status
const useOnlineStatus = () => {
return useSyncExternalStore (
// Subscribe function
( callback ) => {
window. addEventListener ( 'online' , callback);
window. addEventListener ( 'offline' , callback);
return () => {
window. removeEventListener ( 'online' , callback);
window. removeEventListener ( 'offline' , callback);
};
},
// getSnapshot function
() => navigator.onLine,
// getServerSnapshot (SSR)
() => true
);
};
// Usage
const StatusIndicator = () => {
const isOnline = useOnlineStatus ();
return (
< div >
{isOnline ? '🟢 Online' : '🔴 Offline' }
</ div >
);
};
// Window width hook
const useWindowWidth = () => {
return useSyncExternalStore (
( callback ) => {
window. addEventListener ( 'resize' , callback);
return () => window. removeEventListener ( 'resize' , callback);
},
() => window.innerWidth,
() => 0 // SSR default
);
};
// Custom store example
class CountStore {
constructor () {
this .count = 0 ;
this .listeners = new Set ();
}
subscribe = ( callback ) => {
this .listeners. add (callback);
return () => this .listeners. delete (callback);
};
getSnapshot = () => {
return this .count;
};
increment = () => {
this .count ++ ;
this .listeners. forEach ( listener => listener ());
};
}
const store = new CountStore ();
const useCountStore = () => {
return useSyncExternalStore (
store.subscribe,
store.getSnapshot
);
};
const Counter = () => {
const count = useCountStore ();
return (
< div >
< p >Count: {count}</ p >
< button onClick = {() => store. increment ()}>+</ button >
</ div >
);
};
// Local storage sync
const useLocalStorage = ( key , initialValue ) => {
return useSyncExternalStore (
( callback ) => {
const handler = ( e ) => {
if (e.key === key) callback ();
};
window. addEventListener ( 'storage' , handler);
return () => window. removeEventListener ( 'storage' , handler);
},
() => {
try {
return JSON . parse (localStorage. getItem (key)) ?? initialValue;
} catch {
return initialValue;
}
},
() => initialValue
);
};
Note: useSyncExternalStore is primarily for library authors.
Most apps should use built-in hooks or state management libraries that already use this hook internally.
3.5 useInsertionEffect Hook for CSS-in-JS Libraries NEW
Feature
Syntax
Description
Use Case
useInsertionEffect
useInsertionEffect(() => {}, [deps])
Insert styles before layout effects
CSS-in-JS libraries only
Timing
Before useLayoutEffect
Runs before DOM mutations are visible
Inject <style> tags
No DOM Access
Cannot read/write DOM
DOM not yet updated
Style injection only
Library Use Only
Not for application code
Advanced library feature
styled-components, emotion
Example: useInsertionEffect for style injection
import { useInsertionEffect } from 'react' ;
// CSS-in-JS library example (simplified)
const styleCache = new Map ();
const useStyleInjection = ( css , id ) => {
useInsertionEffect (() => {
// Check if style already injected
if (styleCache. has (id)) {
return ;
}
// Create and inject style tag
const style = document. createElement ( 'style' );
style.textContent = css;
style. setAttribute ( 'data-style-id' , id);
document.head. appendChild (style);
// Cache it
styleCache. set (id, style);
// Cleanup
return () => {
document.head. removeChild (style);
styleCache. delete (id);
};
}, [css, id]);
};
// Example usage in a CSS-in-JS library
const useStyledComponent = ( styles ) => {
const id = useId ();
const className = \ `styled- \$ {id} \` ;
const css = \` .styled- \$ {id} {
\$ {Object.entries(styles)
.map(([key, value]) => \`\$ {key}: \$ {value}; \` )
.join(' \n ')}
} \` ;
useStyleInjection(css, id);
return className;
};
// Usage in components
const MyComponent = () => {
const className = useStyledComponent({
'background-color': '#f0f0f0',
'padding': '20px',
'border-radius': '8px'
});
return <div className={className}>Styled Content</div>;
};
// Real-world pattern (styled-components-like)
const styled = (tag) => (styles) => {
return (props) => {
const id = useId();
const className = \` sc- \$ {id} \` ;
useInsertionEffect(() => {
const css = generateCSS(className, styles, props);
injectStyle(css, id);
return () => removeStyle(id);
}, [className, styles, props]);
return React.createElement(tag, {
...props,
className: \`\$ {className} \$ {props.className || ''} \`
});
};
};
// Usage
const Button = styled('button') \`
background: blue;
color: white;
padding: 10px 20px;
\` ;
// In component
<Button>Click Me</Button>
Warning: useInsertionEffect is ONLY for CSS-in-JS library authors. Regular
applications should never use this hook. Use useEffect or useLayoutEffect instead.
3.6 Concurrent Features and Automatic Batching NEW
Feature
Description
Benefit
Example
Automatic Batching
Multiple state updates batched automatically
Fewer renders, better performance
Event handlers, timeouts, promises
Concurrent Rendering
React can interrupt rendering work
Keep UI responsive during updates
Large list updates
Transitions
Mark updates as non-urgent
Prioritize urgent updates
useTransition, startTransition
Streaming SSR
Send HTML in chunks as ready
Faster Time to First Byte
Suspense on server
Selective Hydration
Hydrate components as user interacts
Faster Time to Interactive
Priority-based hydration
Strict Mode Double Render
Effects run twice in development
Catch side effect bugs early
Development only
Example: Automatic batching in React 18
import { useState } from 'react' ;
import { flushSync } from 'react-dom' ;
// React 18: Automatic batching everywhere
const AutoBatchingExample = () => {
const [ count , setCount ] = useState ( 0 );
const [ flag , setFlag ] = useState ( false );
console. log ( 'Render' ); // Only logs once per click
// Event handlers - batched (React 17 too)
const handleClick = () => {
setCount ( c => c + 1 );
setFlag ( f => ! f);
// Only 1 render
};
// Timeouts - NOW batched (was 2 renders in React 17)
const handleTimeout = () => {
setTimeout (() => {
setCount ( c => c + 1 );
setFlag ( f => ! f);
// Only 1 render in React 18!
}, 1000 );
};
// Promises - NOW batched (was 2 renders in React 17)
const handleAsync = async () => {
const data = await fetchData ();
setCount (data.count);
setFlag (data.flag);
// Only 1 render in React 18!
};
// Native event handlers - NOW batched
useEffect (() => {
const handler = () => {
setCount ( c => c + 1 );
setFlag ( f => ! f);
// Only 1 render in React 18!
};
window. addEventListener ( 'resize' , handler);
return () => window. removeEventListener ( 'resize' , handler);
}, []);
// Opt-out of batching with flushSync (rare)
const handleNoFlush = () => {
flushSync (() => {
setCount ( c => c + 1 );
});
// React renders here
flushSync (() => {
setFlag ( f => ! f);
});
// React renders again - 2 total renders
};
return (
< div >
< p >Count: {count}</ p >
< p >Flag: {flag. toString ()}</ p >
< button onClick = {handleClick}>Update</ button >
< button onClick = {handleTimeout}>Update (Timeout)</ button >
< button onClick = {handleAsync}>Update (Async)</ button >
</ div >
);
};
// Concurrent features example
const ConcurrentExample = () => {
const [ resource , setResource ] = useState (initialResource);
// Wrap in transition for smooth updates
const handleUpdate = () => {
startTransition (() => {
setResource ( fetchNewResource ());
});
};
return (
< Suspense fallback = {< Spinner />}>
< button onClick = {handleUpdate}>Update</ button >
< ResourceComponent resource = {resource} />
</ Suspense >
);
};
// Strict Mode double render
const StrictModeExample = () => {
useEffect (() => {
console. log ( 'Effect' );
// In development, logs twice to help find bugs
return () => {
console. log ( 'Cleanup' );
// Also runs twice in development
};
}, []);
return < div >Component</ div >;
};
React 18 Key Improvements
Automatic Batching : Multiple state updates batched everywhere (promises,
timeouts, native events)
Concurrent Rendering : React can pause and resume work, keeping UI
responsive
useTransition : Mark updates as non-urgent for better UX
useDeferredValue : Defer expensive updates while keeping input responsive
Streaming SSR : Send HTML in chunks for faster initial load
Selective Hydration : Prioritize hydration based on user interaction
Note: React 18's concurrent features are opt-in. Use createRoot
instead of render to enable them:
ReactDOM.createRoot(container).render(<App />)
4. Component State Management Patterns
4.1 Local State with useState and useReducer
Pattern
When to Use
Pros
Cons
useState
Simple independent state values
Simple API, easy to understand, minimal boilerplate
Can get messy with complex state
useReducer
Complex state logic, multiple sub-values
Centralized logic, predictable updates, testable
More boilerplate, steeper learning curve
Multiple useState
Unrelated state pieces
Clear separation of concerns
Many setters, potential for mistakes
Object State
Related properties that update together
Single state object, atomic updates
Must spread previous state carefully
State Initializer
Expensive initial computation
Computed only once
Function call syntax needed
Example: Choosing between useState and useReducer
// useState for simple state
const SimpleCounter = () => {
const [ count , setCount ] = useState ( 0 );
return < button onClick = {() => setCount (count + 1 )}>{count}</ button >;
};
// Multiple useState for independent values
const UserForm = () => {
const [ name , setName ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
const [ age , setAge ] = useState ( 0 );
// Each state is independent and clear
return ( /* form */ );
};
// useState with object for related data
const ProfileEditor = () => {
const [ profile , setProfile ] = useState ({
name: '' ,
email: '' ,
bio: ''
});
const updateField = ( field , value ) => {
setProfile ( prev => ({ ... prev, [field]: value }));
};
return ( /* editor */ );
};
// useReducer for complex state logic
const ShoppingCart = () => {
const [ state , dispatch ] = useReducer (cartReducer, {
items: [],
total: 0 ,
discount: 0 ,
tax: 0
});
return (
< div >
< button onClick = {() => dispatch ({
type: 'ADD_ITEM' ,
payload: item
})}>
Add to Cart
</ button >
< button onClick = {() => dispatch ({ type: 'APPLY_DISCOUNT' })}>
Apply Discount
</ button >
< button onClick = {() => dispatch ({ type: 'CALCULATE_TOTAL' })}>
Calculate Total
</ button >
</ div >
);
};
// Decision tree:
// - 1 value, simple updates? → useState
// - Few independent values? → multiple useState
// - Related values updating together? → useState with object
// - Complex logic, state depends on actions? → useReducer
// - State transitions like a state machine? → useReducer
4.2 State Lifting and Sharing Between Components
Technique
Description
Use Case
Trade-off
Lift State Up
Move state to common parent component
Share state between sibling components
Parent re-renders on every update
Prop Drilling
Pass state through component tree
Simple component hierarchies
Tedious with deep nesting
Inverse Data Flow
Child updates parent via callbacks
Child needs to modify parent state
Callback props everywhere
Composition
Pass components as children/props
Avoid drilling through wrappers
Requires planning component structure
Context
Global state without prop drilling
Deep nesting, many consumers
Can cause unnecessary re-renders
Example: State lifting patterns
// Before: State in child components (not shared)
const TemperatureInput1 = () => {
const [ temp , setTemp ] = useState ( '' );
return < input value = {temp} onChange = {( e ) => setTemp (e.target.value)} />;
};
// After: Lift state to parent
const TemperatureConverter = () => {
const [ celsius , setCelsius ] = useState ( '' );
const fahrenheit = celsius ? (celsius * 9 / 5 + 32 ). toFixed ( 1 ) : '' ;
return (
< div >
< TemperatureInput
scale = "Celsius"
temperature = {celsius}
onTemperatureChange = {setCelsius}
/>
< TemperatureInput
scale = "Fahrenheit"
temperature = {fahrenheit}
onTemperatureChange = {( f ) => setCelsius (((f - 32 ) * 5 / 9 ). toFixed ( 1 ))}
/>
< p >Boiling: {celsius >= 100 ? 'Yes' : 'No' }</ p >
</ div >
);
};
const TemperatureInput = ({ scale , temperature , onTemperatureChange }) => (
< div >
< label >{scale}:</ label >
< input
value = {temperature}
onChange = {( e ) => onTemperatureChange (e.target.value)}
/>
</ div >
);
// Composition to avoid prop drilling
const Layout = ({ header , sidebar , content }) => (
< div >
< header >{header}</ header >
< div className = "main" >
< aside >{sidebar}</ aside >
< main >{content}</ main >
</ div >
</ div >
);
// Usage: Pass components with their own state
const App = () => {
const [ user , setUser ] = useState ( null );
return (
< Layout
header = {< Header user = {user} onLogout = {() => setUser ( null )} />}
sidebar = {< Sidebar />} { /* No user prop needed */ }
content = {< Dashboard user = {user} />}
/>
);
};
// When to lift state:
// ✓ Two+ components need same data
// ✓ Data needs to be synchronized
// ✓ Parent needs to coordinate children
// ✗ Only one component uses it (keep local)
// ✗ Performance issues (consider Context/memo)
4.3 State Colocation and Component Responsibility
Principle
Description
Benefit
Example
State Colocation
Keep state as close to where it's used
Better performance, easier maintenance
Modal open state in Modal component
Single Responsibility
Component manages its own concerns
Reusable, testable, maintainable
Form validates its own fields
Lift on Demand
Start local, lift only when needed
Avoid premature optimization
Lift when sibling needs access
Encapsulation
Hide implementation details
Flexibility to change internals
Accordion manages panel states
Separation of Concerns
Split unrelated state into components
Independent testing and changes
Filter state vs display state
Example: State colocation best practices
// ❌ Bad: State too high in tree
const App = () => {
const [ modalOpen , setModalOpen ] = useState ( false );
const [ tooltipOpen , setTooltipOpen ] = useState ( false );
const [ accordionIndex , setAccordionIndex ] = useState ( 0 );
return (
< div >
< Modal open = {modalOpen} onClose = {() => setModalOpen ( false )} />
< Tooltip open = {tooltipOpen} />
< Accordion activeIndex = {accordionIndex} />
</ div >
);
};
// ✓ Good: State colocated with components
const Modal = () => {
const [ isOpen , setIsOpen ] = useState ( false );
return (
<>
< button onClick = {() => setIsOpen ( true )}>Open</ button >
{isOpen && (
< div className = "modal" >
< button onClick = {() => setIsOpen ( false )}>Close</ button >
</ div >
)}
</>
);
};
// ✓ Good: Component responsibility
const SearchableList = ({ items }) => {
// Search state belongs here
const [ query , setQuery ] = useState ( '' );
const [ sortOrder , setSortOrder ] = useState ( 'asc' );
const filteredItems = useMemo (() => {
return items
. filter ( item => item.name. includes (query))
. sort (( a , b ) => sortOrder === 'asc'
? a.name. localeCompare (b.name)
: b.name. localeCompare (a.name)
);
}, [items, query, sortOrder]);
return (
< div >
< input value = {query} onChange = {( e ) => setQuery (e.target.value)} />
< button onClick = {() => setSortOrder ( prev =>
prev === 'asc' ? 'desc' : 'asc'
)}>
Sort
</ button >
< List items = {filteredItems} />
</ div >
);
};
// ✓ Good: Lift only when necessary
const ProductPage = () => {
// Shared between multiple components
const [ selectedProduct , setSelectedProduct ] = useState ( null );
return (
< div >
< ProductList onSelect = {setSelectedProduct} />
< ProductDetails product = {selectedProduct} />
< RelatedProducts productId = {selectedProduct?.id} />
</ div >
);
};
// Guidelines:
// 1. Start with local state
// 2. Lift when siblings need to share
// 3. Keep state close to usage
// 4. Each component owns its UI state
Note: State colocation improves performance because only
components that need the state will re-render. Avoid lifting state too early—do it when actually needed.
4.4 Derived State and Computed Values
Technique
Implementation
Use Case
Performance
Inline Calculation
const total = items.reduce(...)
Simple, cheap calculations
Recalculated every render
useMemo
useMemo(() => calc, [deps])
Expensive computations
Cached between renders
Avoid Sync State
Don't copy props to state
Prevent sync issues
No duplication overhead
Controlled Derivation
Derive in render, don't store
Always in sync with source
Depends on calculation cost
Key Reset Pattern
<Comp key={id} />
Reset state when prop changes
Component remounts
Example: Derived state patterns
// ✓ Good: Derive inline (cheap calculation)
const CartSummary = ({ items }) => {
const total = items. reduce (( sum , item ) => sum + item.price, 0 );
const itemCount = items. length ;
const tax = total * 0.1 ;
return (
< div >
< p >Items: {itemCount}</ p >
< p >Subtotal: ${total}</ p >
< p >Tax: ${tax}</ p >
< p >Total: ${total + tax}</ p >
</ div >
);
};
// ✓ Good: useMemo for expensive calculations
const DataTable = ({ data , filters }) => {
const filteredData = useMemo (() => {
return data
. filter ( row => filters. every ( f => f (row)))
. sort (( a , b ) => a.name. localeCompare (b.name))
. map ( row => transformRow (row));
}, [data, filters]);
return < table >{ /* render filteredData */ }</ table >;
};
// ❌ Bad: Syncing props to state
const SearchInput = ({ initialValue }) => {
// DON'T DO THIS
const [ value , setValue ] = useState (initialValue);
// value won't update when initialValue changes!
return < input value = {value} onChange = {( e ) => setValue (e.target.value)} />;
};
// ✓ Good: Controlled component
const SearchInput = ({ value , onChange }) => {
return < input value = {value} onChange = {onChange} />;
};
// ✓ Good: Key reset pattern when prop changes
const UserProfile = ({ userId }) => {
return < ProfileForm key = {userId} userId = {userId} />;
// ProfileForm remounts and resets state when userId changes
};
// ❌ Bad: Storing derived state
const FilteredList = ({ items , filter }) => {
const [ filteredItems , setFilteredItems ] = useState ([]);
// This creates sync issues!
useEffect (() => {
setFilteredItems (items. filter (filter));
}, [items, filter]);
return < List items = {filteredItems} />;
};
// ✓ Good: Derive on every render
const FilteredList = ({ items , filter }) => {
const filteredItems = useMemo (
() => items. filter (filter),
[items, filter]
);
return < List items = {filteredItems} />;
};
// Rule: If you can calculate it from props/state, don't store it!
Warning: Never copy props to state unless you explicitly want to ignore
prop updates . This causes sync bugs. Derive values instead or use the key
prop to reset component state.
4.5 State Normalization for Complex Data
Pattern
Structure
Benefit
Use Case
Normalized State
{ byId: {}, allIds: [] }
O(1) lookups, no duplication
Large datasets, frequent updates
Flat Structure
Avoid deep nesting
Easier updates, better performance
Nested entities
Entity Tables
Separate entities by type
Clear organization, type safety
Multiple entity types
Reference by ID
Store IDs instead of objects
Single source of truth
Relationships between entities
Selector Functions
Compute derived views
Reusable query logic
Complex filtering/sorting
Example: State normalization patterns (Part 1/2)
// ❌ Bad: Nested, duplicated data
const badState = {
posts: [
{
id: 1 ,
title: 'Post 1' ,
author: { id: 1 , name: 'Alice' },
comments: [
{ id: 1 , text: 'Comment' , author: { id: 2 , name: 'Bob' } }
]
}
]
};
// Problems: Deep nesting, author duplicated, hard to update
// ✓ Good: Normalized structure
const normalizedState = {
users: {
byId: {
1 : { id: 1 , name: 'Alice' },
2 : { id: 2 , name: 'Bob' }
},
allIds: [ 1 , 2 ]
},
posts: {
byId: {
1 : { id: 1 , title: 'Post 1' , authorId: 1 , commentIds: [ 1 ] }
},
allIds: [ 1 ]
},
comments: {
byId: {
1 : { id: 1 , text: 'Comment' , authorId: 2 , postId: 1 }
},
allIds: [ 1 ]
}
};
// Reducer for normalized state
const postsReducer = ( state , action ) => {
switch (action.type) {
case 'ADD_POST' :
return {
... state,
byId: {
... state.byId,
[action.payload.id]: action.payload
},
allIds: [ ... state.allIds, action.payload.id]
};
case 'UPDATE_POST' :
return {
... state,
byId: {
... state.byId,
[action.payload.id]: {
... state.byId[action.payload.id],
... action.payload.updates
}
}
};
case 'DELETE_POST' :
const { [action.payload]: deleted , ... remainingById } = state.byId;
return {
byId: remainingById,
allIds: state.allIds. filter ( id => id !== action.payload)
};
default :
return state;
}
};
Example: State normalization patterns (Part 2/2)
// Selector functions
const selectPostById = ( state , postId ) => state.posts.byId[postId];
const selectPostWithAuthor = ( state , postId ) => {
const post = state.posts.byId[postId];
const author = state.users.byId[post.authorId];
return { ... post, author };
};
const selectPostsWithComments = ( state ) => {
return state.posts.allIds. map ( postId => {
const post = state.posts.byId[postId];
const comments = post.commentIds. map (
commentId => state.comments.byId[commentId]
);
return { ... post, comments };
});
};
// Component usage
const PostList = () => {
const [ state , dispatch ] = useReducer (postsReducer, initialState);
const posts = state.posts.allIds. map ( id => state.posts.byId[id]);
return (
< div >
{posts. map ( post => (
< Post
key = {post.id}
post = {post}
author = {state.users.byId[post.authorId]}
onUpdate = {( updates ) => dispatch ({
type: 'UPDATE_POST' ,
payload: { id: post.id, updates }
})}
/>
))}
</ div >
);
};
// Benefits of normalization:
// ✓ Single source of truth
// ✓ Fast lookups by ID
// ✓ Easy updates (no searching)
// ✓ No data duplication
// ✓ Better performance
Note: Consider normalization when you have: (1) nested entities, (2) the same entity in
multiple places, (3) frequent updates to entities, (4) need for fast lookups. Libraries like normalizr can help.
4.6 State Machines and useReducer Patterns
Concept
Description
Benefit
Example
State Machine
Finite set of states and transitions
Predictable, testable state logic
Loading, success, error states
Valid Transitions
Only allow specific state changes
Prevent impossible states
Can't be loading and success
Action-based Updates
Dispatch actions to trigger transitions
Clear intent, easier debugging
FETCH, SUCCESS, ERROR actions
State Context
Additional data within each state
Rich state information
Error message in error state
XState Integration
Formal state machine library
Visualization, advanced features
Complex workflows
Example: State machine patterns with useReducer (Part 1/2)
// State machine for data fetching
const STATES = {
IDLE: 'idle' ,
LOADING: 'loading' ,
SUCCESS: 'success' ,
ERROR: 'error'
};
const fetchReducer = ( state , action ) => {
switch (action.type) {
case 'FETCH' :
return { status: STATES . LOADING , data: null , error: null };
case 'SUCCESS' :
return { status: STATES . SUCCESS , data: action.payload, error: null };
case 'ERROR' :
return { status: STATES . ERROR , data: null , error: action.payload };
case 'RESET' :
return { status: STATES . IDLE , data: null , error: null };
default :
return state;
}
};
const DataFetcher = ({ url }) => {
const [ state , dispatch ] = useReducer (fetchReducer, {
status: STATES . IDLE ,
data: null ,
error: null
});
const fetchData = async () => {
dispatch ({ type: 'FETCH' });
try {
const response = await fetch (url);
const data = await response. json ();
dispatch ({ type: 'SUCCESS' , payload: data });
} catch (error) {
dispatch ({ type: 'ERROR' , payload: error.message });
}
};
// Render based on state
if (state.status === STATES . IDLE ) {
return < button onClick = {fetchData}>Load Data</ button >;
}
if (state.status === STATES . LOADING ) {
return < div >Loading...</ div >;
}
if (state.status === STATES . ERROR ) {
return (
< div >
Error: {state.error}
< button onClick = {fetchData}>Retry</ button >
</ div >
);
}
return (
< div >
< pre >{ JSON . stringify (state.data, null , 2 )}</ pre >
< button onClick = {() => dispatch ({ type: 'RESET' })}>Reset</ button >
</ div >
);
};
Example: State machine patterns with useReducer (Part 2/2)
// Complex state machine: Form wizard
const WIZARD_STATES = {
PERSONAL_INFO: 'personalInfo' ,
ADDRESS: 'address' ,
PAYMENT: 'payment' ,
REVIEW: 'review' ,
SUBMITTING: 'submitting' ,
SUCCESS: 'success' ,
ERROR: 'error'
};
const wizardReducer = ( state , action ) => {
switch (action.type) {
case 'NEXT' :
const nextStates = {
[ WIZARD_STATES . PERSONAL_INFO ]: WIZARD_STATES . ADDRESS ,
[ WIZARD_STATES . ADDRESS ]: WIZARD_STATES . PAYMENT ,
[ WIZARD_STATES . PAYMENT ]: WIZARD_STATES . REVIEW
};
return { ... state, currentStep: nextStates[state.currentStep] };
case 'BACK' :
const prevStates = {
[ WIZARD_STATES . ADDRESS ]: WIZARD_STATES . PERSONAL_INFO ,
[ WIZARD_STATES . PAYMENT ]: WIZARD_STATES . ADDRESS ,
[ WIZARD_STATES . REVIEW ]: WIZARD_STATES . PAYMENT
};
return { ... state, currentStep: prevStates[state.currentStep] };
case 'UPDATE_DATA' :
return {
... state,
formData: { ... state.formData, ... action.payload }
};
case 'SUBMIT' :
return { ... state, currentStep: WIZARD_STATES . SUBMITTING };
case 'SUBMIT_SUCCESS' :
return { ... state, currentStep: WIZARD_STATES . SUCCESS };
case 'SUBMIT_ERROR' :
return {
... state,
currentStep: WIZARD_STATES . ERROR ,
error: action.payload
};
default :
return state;
}
};
// Benefits of state machines:
// ✓ Impossible states become impossible
// ✓ Clear transitions between states
// ✓ Easy to test each state
// ✓ Visual representation possible
// ✓ Predictable behavior
State Management Decision Guide
Local component state : Use useState for simple values, useReducer for
complex logic
Sharing state : Lift to common parent, use composition to avoid prop
drilling
State location : Keep state close to where it's used (colocation)
Derived values : Calculate from existing state, don't duplicate
Complex data : Normalize structure with byId/allIds pattern
State machines : Use for well-defined state transitions and workflows
5. React Context API and Global State
5.1 createContext and Provider Components
Feature
Syntax
Description
Use Case
createContext
const Ctx = createContext(defaultValue)
Create context object
Define shared state container
Default Value
createContext(initialValue)
Fallback when no provider exists
Testing, development defaults
Provider Component
<Ctx.Provider value={...}>
Provide context value to children
Share data down component tree
Provider Value
value={{ state, actions }}
Object with state and updater functions
Complete state management
Nested Providers
<A><B><C /></B></A>
Stack multiple providers
Compose contexts
Provider Pattern
Wrap App with providers
Make context available globally
App-wide state
Example: Creating and providing context
import { createContext, useState } from 'react' ;
// 1. Create context with default value
const ThemeContext = createContext ({
theme: 'light' ,
toggleTheme : () => {}
});
// 2. Create provider component
export const ThemeProvider = ({ children }) => {
const [ theme , setTheme ] = useState ( 'light' );
const toggleTheme = () => {
setTheme ( prev => prev === 'light' ? 'dark' : 'light' );
};
const value = {
theme,
toggleTheme
};
return (
< ThemeContext.Provider value = {value}>
{children}
</ ThemeContext.Provider >
);
};
// 3. Create auth provider
const AuthContext = createContext ( null );
export const AuthProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
const login = async ( credentials ) => {
setLoading ( true );
const userData = await api. login (credentials);
setUser (userData);
setLoading ( false );
};
const logout = () => {
setUser ( null );
};
return (
< AuthContext.Provider value = {{ user, loading, login, logout }}>
{children}
</ AuthContext.Provider >
);
};
// 4. Compose providers in App
const App = () => (
< ThemeProvider >
< AuthProvider >
< Router >
< Layout />
</ Router >
</ AuthProvider >
</ ThemeProvider >
);
// 5. Provider composition helper
const ComposeProviders = ({ providers , children }) => {
return providers. reduceRight (
( acc , Provider ) => < Provider >{acc}</ Provider >,
children
);
};
// Usage
< ComposeProviders providers = {[ThemeProvider, AuthProvider, RouterProvider]}>
< App />
</ ComposeProviders >
5.2 useContext Hook and Context Consumption
Pattern
Syntax
Description
Best Practice
useContext Hook
const value = useContext(Context)
Access context value in component
Must be inside Provider
Destructure Value
const { state, action } = useContext(Ctx)
Extract specific values from context
Cleaner code, clear dependencies
Custom Hook
const useMyContext = () => useContext(Ctx)
Wrap useContext in custom hook
Better error handling, abstraction
Null Check
if (!context) throw Error
Ensure provider exists
Catch missing provider early
Multiple Contexts
Call useContext multiple times
Consume different contexts
Separate concerns
Example: Consuming context with custom hooks
import { createContext, useContext, useState } from 'react' ;
// Context setup
const UserContext = createContext ( null );
export const UserProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
return (
< UserContext.Provider value = {{ user, setUser }}>
{children}
</ UserContext.Provider >
);
};
// ✓ Good: Custom hook with error handling
export const useUser = () => {
const context = useContext (UserContext);
if ( ! context) {
throw new Error ( 'useUser must be used within UserProvider' );
}
return context;
};
// Usage in components
const Profile = () => {
const { user , setUser } = useUser ();
if ( ! user) return < Login />;
return (
< div >
< h1 >{user.name}</ h1 >
< button onClick = {() => setUser ( null )}>Logout</ button >
</ div >
);
};
// Multiple contexts example
const ThemeContext = createContext ( 'light' );
const LanguageContext = createContext ( 'en' );
const useTheme = () => useContext (ThemeContext);
const useLanguage = () => useContext (LanguageContext);
const Header = () => {
const theme = useTheme ();
const language = useLanguage ();
const { user } = useUser ();
return (
< header className = {theme}>
< span >{language === 'en' ? 'Welcome' : 'Bienvenido' }</ span >
{user && < span >{user.name}</ span >}
</ header >
);
};
// Selective context consumption
const Button = () => {
// Only subscribes to theme, not user
const theme = useTheme ();
return < button className = {theme}>Click</ button >;
};
// Pattern: Context + Reducer
const TodoContext = createContext ( null );
export const TodoProvider = ({ children }) => {
const [ state , dispatch ] = useReducer (todoReducer, initialState);
return (
< TodoContext.Provider value = {{ state, dispatch }}>
{children}
</ TodoContext.Provider >
);
};
export const useTodos = () => {
const context = useContext (TodoContext);
if ( ! context) throw new Error ( 'useTodos requires TodoProvider' );
return context;
};
5.3 Multiple Contexts and Context Composition
Pattern
Description
Benefit
Example
Separate Contexts
Different contexts for different concerns
Clear separation, selective updates
Theme, Auth, Language contexts
Nested Providers
Stack providers in component tree
Layer functionality
Auth wraps Theme wraps App
Context Composition
Combine multiple contexts in one hook
Convenient API, related data
useApp() returns theme + auth
Scoped Contexts
Context for specific feature area
Isolated state, no global pollution
Form context, Modal context
Context Hierarchy
Parent contexts provide to children
Natural data flow
App → Feature → Component
Example: Multiple contexts and composition
// Separate contexts for different concerns
const ThemeContext = createContext ( 'light' );
const AuthContext = createContext ( null );
const NotificationContext = createContext ( null );
// Provider setup
const App = () => (
< ThemeProvider >
< AuthProvider >
< NotificationProvider >
< Router />
</ NotificationProvider >
</ AuthProvider >
</ ThemeProvider >
);
// Composed hook for convenience
const useAppContext = () => {
const theme = useContext (ThemeContext);
const auth = useContext (AuthContext);
const notifications = useContext (NotificationContext);
return { theme, auth, notifications };
};
// Usage
const Dashboard = () => {
const { theme , auth , notifications } = useAppContext ();
return (
< div className = {theme}>
< h1 >Welcome {auth.user.name}</ h1 >
{notifications.messages. map ( msg => (
< div key = {msg.id}>{msg.text}</ div >
))}
</ div >
);
};
// Scoped context for features
const FormContext = createContext ( null );
const FormProvider = ({ children , initialValues }) => {
const [ values , setValues ] = useState (initialValues);
const [ errors , setErrors ] = useState ({});
const setValue = ( field , value ) => {
setValues ( prev => ({ ... prev, [field]: value }));
};
return (
< FormContext.Provider value = {{ values, errors, setValue }}>
{children}
</ FormContext.Provider >
);
};
// Form uses scoped context
const MyForm = () => (
< FormProvider initialValues = {{ name: '' , email: '' }}>
< FormFields />
< SubmitButton />
</ FormProvider >
);
// Hierarchical contexts
const AppContext = createContext ( null );
const FeatureContext = createContext ( null );
const FeatureArea = () => {
const appData = useContext (AppContext);
const [ featureState , setFeatureState ] = useState ({});
return (
< FeatureContext.Provider value = {{ appData, featureState, setFeatureState }}>
< FeatureComponents />
</ FeatureContext.Provider >
);
};
// Provider composition utility
const combineProviders = ( ... providers ) => {
return ({ children }) => {
return providers. reduce (
( acc , [ Provider , props ]) => (
< Provider { ... props}>{acc}</ Provider >
),
children
);
};
};
// Usage
const AppProviders = combineProviders (
[ThemeProvider, { defaultTheme: 'light' }],
[AuthProvider, {}],
[NotificationProvider, { position: 'top-right' }]
);
< AppProviders >
< App />
</ AppProviders >
5.4 Context Performance and Re-render Optimization
Issue
Cause
Solution
Pattern
Unnecessary Re-renders
Provider value changes on every render
Memoize provider value
useMemo(() => ({ state }), [state])
All Consumers Update
Any context change triggers all consumers
Split contexts by update frequency
Separate read-only from mutable state
Large Context Objects
Single context with many properties
Multiple smaller contexts
Theme, Auth, Settings as separate
Function References
New functions created every render
useCallback for updater functions
useCallback(() => {}, [])
Deep Component Trees
Many components between provider/consumer
React.memo on intermediate components
Prevent propagation of re-renders
Example: Context performance optimization
// ❌ Bad: New object every render
const BadProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
// This creates a new object on every render!
return (
< Context.Provider value = {{ count, setCount }}>
{children}
</ Context.Provider >
);
};
// ✓ Good: Memoized value
const GoodProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
const value = useMemo (
() => ({ count, setCount }),
[count] // Only recreate when count changes
);
return (
< Context.Provider value = {value}>
{children}
</ Context.Provider >
);
};
// ✓ Better: Split contexts
const CountContext = createContext ( 0 );
const CountDispatchContext = createContext ( null );
const CountProvider = ({ children }) => {
const [ count , setCount ] = useState ( 0 );
// State and dispatch are separate contexts
return (
< CountContext.Provider value = {count}>
< CountDispatchContext.Provider value = {setCount}>
{children}
</ CountDispatchContext.Provider >
</ CountContext.Provider >
);
};
// Components can subscribe to only what they need
const DisplayCount = () => {
const count = useContext (CountContext); // Only re-renders when count changes
return < div >{count}</ div >;
};
const IncrementButton = () => {
const setCount = useContext (CountDispatchContext); // Never re-renders!
return < button onClick = {() => setCount ( c => c + 1 )}>+</ button >;
};
// ✓ Good: Memoized callbacks
const UserProvider = ({ children }) => {
const [ user , setUser ] = useState ( null );
const login = useCallback ( async ( credentials ) => {
const userData = await api. login (credentials);
setUser (userData);
}, []);
const logout = useCallback (() => {
setUser ( null );
}, []);
const value = useMemo (
() => ({ user, login, logout }),
[user, login, logout]
);
return (
< UserContext.Provider value = {value}>
{children}
</ UserContext.Provider >
);
};
// Memo intermediate components
const Layout = memo (({ children }) => {
return (
< div className = "layout" >
< Sidebar />
< main >{children}</ main >
</ div >
);
});
// Split by update frequency
const StaticConfigContext = createContext ( null ); // Rarely changes
const DynamicDataContext = createContext ( null ); // Changes often
const App = () => (
< StaticConfigContext.Provider value = {config}>
< DynamicDataProvider >
< Routes />
</ DynamicDataProvider >
</ StaticConfigContext.Provider >
);
Warning: Context triggers re-renders in ALL consumers when value changes. Use
useMemo
for provider values, useCallback for functions, and split contexts to minimize unnecessary updates.
5.5 Context vs Prop Drilling Trade-offs
Approach
Pros
Cons
When to Use
Props
Explicit, type-safe, easy to trace, testable
Verbose with deep nesting, refactoring burden
2-3 levels deep, clear data flow needed
Context
Avoid drilling, cleaner intermediate components
Implicit dependencies, harder to trace, re-render issues
Deep nesting, many consumers, global state
Composition
Avoid both drilling and context, flexible
Requires component structure planning
Layout components, wrapper abstraction
State Management
Powerful features, DevTools, middleware
Boilerplate, learning curve, bundle size
Complex apps, time-travel debugging
URL State
Shareable, persistent, no drilling
Limited to serializable data, URL length
Filters, pagination, tabs
Example: Comparing approaches
// Approach 1: Props (explicit but verbose)
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return < Layout theme = {theme} setTheme = {setTheme} />;
};
const Layout = ({ theme , setTheme }) => (
< div >
< Header theme = {theme} setTheme = {setTheme} />
< Content theme = {theme} />
</ div >
);
const Header = ({ theme , setTheme }) => (
< header >
< ThemeToggle theme = {theme} setTheme = {setTheme} />
</ header >
);
// Approach 2: Context (implicit but cleaner)
const ThemeContext = createContext ( 'light' );
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return (
< ThemeContext.Provider value = {{ theme, setTheme }}>
< Layout />
</ ThemeContext.Provider >
);
};
const Layout = () => (
< div >
< Header />
< Content />
</ div >
);
const ThemeToggle = () => {
const { theme , setTheme } = useContext (ThemeContext);
return < button onClick = {() => setTheme (theme === 'light' ? 'dark' : 'light' )}>Toggle</ button >;
};
// Approach 3: Composition (flexible)
const App = () => {
const [ theme , setTheme ] = useState ( 'light' );
return (
< Layout
header = {< Header theme = {theme} setTheme = {setTheme} />}
content = {< Content theme = {theme} />}
/>
);
};
const Layout = ({ header , content }) => (
< div >
{header}
{content}
</ div >
);
// Decision guide:
// Props: 1-3 levels, clear data flow, explicit dependencies
// Context: 4+ levels, many consumers, truly global state
// Composition: Layout components, avoid wrapper drilling
// State library: Complex state, time-travel, middleware needed
Note: Start with props. Add context when drilling becomes painful (usually 4+ levels). Consider
composition before reaching for context. Reserve state management libraries for truly complex apps.
5.6 Custom Context Hooks and Abstractions
Pattern
Implementation
Benefit
Example
Custom Hook
Wrap useContext in function
Better error messages, validation
useAuth() instead of useContext(AuthContext)
Error Handling
Throw if provider missing
Catch mistakes early
Better DX, clear error messages
Selector Pattern
Custom hooks for subsets of context
Optimized updates, clearer API
useUserName() only subscribes to name
Action Creators
Provide functions not raw dispatch
Type-safe, encapsulated logic
login() instead of dispatch({ type: 'LOGIN' })
Computed Values
Derive values in custom hooks
Reusable logic, memoization
useIsAuthenticated() derives from user state
Example: Custom context hooks and abstractions
// Advanced context pattern with custom hooks
const AuthContext = createContext ( null );
// Provider with reducer
export const AuthProvider = ({ children }) => {
const [ state , dispatch ] = useReducer (authReducer, {
user: null ,
loading: true ,
error: null
});
const value = useMemo (() => ({ state, dispatch }), [state]);
return (
< AuthContext.Provider value = {value}>
{children}
</ AuthContext.Provider >
);
};
// Base hook with error handling
const useAuthContext = () => {
const context = useContext (AuthContext);
if ( ! context) {
throw new Error ( 'useAuth hooks must be used within AuthProvider' );
}
return context;
};
// Public API - action creators
export const useAuth = () => {
const { state , dispatch } = useAuthContext ();
const login = useCallback ( async ( credentials ) => {
dispatch ({ type: 'LOGIN_START' });
try {
const user = await api. login (credentials);
dispatch ({ type: 'LOGIN_SUCCESS' , payload: user });
} catch (error) {
dispatch ({ type: 'LOGIN_ERROR' , payload: error.message });
}
}, [dispatch]);
const logout = useCallback (() => {
dispatch ({ type: 'LOGOUT' });
}, [dispatch]);
return {
user: state.user,
loading: state.loading,
error: state.error,
login,
logout
};
};
// Selector hooks for performance
export const useUser = () => {
const { state } = useAuthContext ();
return state.user;
};
export const useIsAuthenticated = () => {
const { state } = useAuthContext ();
return state.user !== null ;
};
export const useAuthLoading = () => {
const { state } = useAuthContext ();
return state.loading;
};
// Usage in components
const Profile = () => {
const { user , logout } = useAuth ();
return (
< div >
< h1 >{user.name}</ h1 >
< button onClick = {logout}>Logout</ button >
</ div >
);
};
const LoginButton = () => {
const isAuthenticated = useIsAuthenticated ();
const { login } = useAuth ();
if (isAuthenticated) return null ;
return < button onClick = {() => login ({ email, password })}>Login</ button >;
};
// Advanced: Factory for creating context hooks
const createContextHook = ( Context , name ) => {
return () => {
const context = useContext (Context);
if ( ! context) {
throw new Error (\ `use \$ {name} must be used within \$ {name}Provider \` );
}
return context;
};
};
// Usage
const useTheme = createContextHook(ThemeContext, 'Theme');
const useSettings = createContextHook(SettingsContext, 'Settings');
Context API Best Practices
Custom hooks : Always wrap useContext in custom hooks with error handling
Memoization : Use useMemo for provider values and useCallback for functions
Split contexts : Separate by concern and update frequency to optimize
re-renders
Action creators : Provide high-level APIs, hide implementation details
Start simple : Use props first, context for deep drilling (4+ levels)
Composition over context : Consider component composition before context
Input Type
Pattern
State Binding
Event Handler
Text Input
<input value={state} onChange={handler} />
State holds current value
e.target.value
Textarea
<textarea value={state} onChange={handler} />
Same as text input
e.target.value
Checkbox
<input type="checkbox" checked={state} />
Boolean state
e.target.checked
Radio
<input type="radio" checked={state === value} />
Compare state to value
e.target.value
Select
<select value={state} onChange={handler} />
Selected option value
e.target.value
Multiple Select
<select multiple value={array} />
Array of values
Array.from(e.target.selectedOptions)
Example: Controlled component patterns
// Basic text input
const TextInput = () => {
const [ value , setValue ] = useState ( '' );
return (
< input
type = "text"
value = {value}
onChange = {( e ) => setValue (e.target.value)}
placeholder = "Enter text"
/>
);
};
// Multiple inputs with single state object
const Form = () => {
const [ formData , setFormData ] = useState ({
name: '' ,
email: '' ,
age: ''
});
const handleChange = ( e ) => {
const { name , value } = e.target;
setFormData ( prev => ({
... prev,
[name]: value
}));
};
return (
< form >
< input
name = "name"
value = {formData.name}
onChange = {handleChange}
placeholder = "Name"
/>
< input
name = "email"
type = "email"
value = {formData.email}
onChange = {handleChange}
placeholder = "Email"
/>
< input
name = "age"
type = "number"
value = {formData.age}
onChange = {handleChange}
placeholder = "Age"
/>
</ form >
);
};
// Checkbox example
const CheckboxInput = () => {
const [ accepted , setAccepted ] = useState ( false );
return (
< label >
< input
type = "checkbox"
checked = {accepted}
onChange = {( e ) => setAccepted (e.target.checked)}
/>
I accept the terms
</ label >
);
};
// Multiple checkboxes (array)
const MultiCheckbox = () => {
const [ interests , setInterests ] = useState ([]);
const handleToggle = ( interest ) => {
setInterests ( prev =>
prev. includes (interest)
? prev. filter ( i => i !== interest)
: [ ... prev, interest]
);
};
return (
< div >
{[ 'React' , 'Vue' , 'Angular' ]. map ( tech => (
< label key = {tech}>
< input
type = "checkbox"
checked = {interests. includes (tech)}
onChange = {() => handleToggle (tech)}
/>
{tech}
</ label >
))}
</ div >
);
};
// Radio buttons
const RadioInput = () => {
const [ plan , setPlan ] = useState ( 'basic' );
return (
< div >
{[ 'basic' , 'pro' , 'enterprise' ]. map ( value => (
< label key = {value}>
< input
type = "radio"
name = "plan"
value = {value}
checked = {plan === value}
onChange = {( e ) => setPlan (e.target.value)}
/>
{value}
</ label >
))}
</ div >
);
};
// Select dropdown
const SelectInput = () => {
const [ country , setCountry ] = useState ( '' );
return (
< select value = {country} onChange = {( e ) => setCountry (e.target.value)}>
< option value = "" >Select country</ option >
< option value = "us" >United States</ option >
< option value = "uk" >United Kingdom</ option >
< option value = "ca" >Canada</ option >
</ select >
);
};
Note: Controlled components have React state as the "single source of truth". Every state
mutation
happens through setState, providing full control and predictability.
6.2 Uncontrolled Components and useRef
Approach
Implementation
When to Use
Access Pattern
useRef
const ref = useRef(null)
Direct DOM access needed
ref.current.value
defaultValue
<input defaultValue="text" />
Initial value only
No state synchronization
File Input
Always uncontrolled
Security reasons
ref.current.files
Form Reset
formRef.current.reset()
Native form methods
Reset all fields at once
Integration
Third-party libraries
Non-React code
Direct DOM manipulation
Example: Uncontrolled components with useRef
import { useRef } from 'react' ;
// Basic uncontrolled input
const UncontrolledInput = () => {
const inputRef = useRef ( null );
const handleSubmit = ( e ) => {
e. preventDefault ();
console. log ( 'Value:' , inputRef.current.value);
};
return (
< form onSubmit = {handleSubmit}>
< input ref = {inputRef} defaultValue = "Initial value" />
< button type = "submit" >Submit</ button >
</ form >
);
};
// File input (always uncontrolled)
const FileInput = () => {
const fileRef = useRef ( null );
const handleUpload = () => {
const files = fileRef.current.files;
if (files. length > 0 ) {
console. log ( 'Selected file:' , files[ 0 ].name);
}
};
return (
< div >
< input type = "file" ref = {fileRef} />
< button onClick = {handleUpload}>Upload</ button >
</ div >
);
};
// Form with multiple refs
const UncontrolledForm = () => {
const nameRef = useRef ( null );
const emailRef = useRef ( null );
const formRef = useRef ( null );
const handleSubmit = ( e ) => {
e. preventDefault ();
const formData = {
name: nameRef.current.value,
email: emailRef.current.value
};
console. log ( 'Form data:' , formData);
// Reset form using native method
formRef.current. reset ();
};
return (
< form ref = {formRef} onSubmit = {handleSubmit}>
< input ref = {nameRef} name = "name" defaultValue = "" />
< input ref = {emailRef} name = "email" type = "email" defaultValue = "" />
< button type = "submit" >Submit</ button >
< button type = "button" onClick = {() => formRef.current. reset ()}>
Clear
</ button >
</ form >
);
};
// Hybrid approach (controlled + ref for advanced features)
const HybridInput = () => {
const [ value , setValue ] = useState ( '' );
const inputRef = useRef ( null );
const handleFocus = () => {
inputRef.current. focus ();
};
const handleSelectAll = () => {
inputRef.current. select ();
};
return (
< div >
< input
ref = {inputRef}
value = {value}
onChange = {( e ) => setValue (e.target.value)}
/>
< button onClick = {handleFocus}>Focus</ button >
< button onClick = {handleSelectAll}>Select All</ button >
</ div >
);
};
// Integration with third-party library
const ThirdPartyInput = () => {
const containerRef = useRef ( null );
useEffect (() => {
// Initialize third-party library
const instance = ExternalLibrary. init (containerRef.current, {
onChange : ( value ) => {
console. log ( 'Value changed:' , value);
}
});
return () => {
instance. destroy ();
};
}, []);
return < div ref = {containerRef} />;
};
Warning: Prefer controlled components for most use cases. Use uncontrolled only for file
inputs,
integration with non-React code, or when you need direct DOM manipulation.
Strategy
When to Validate
Implementation
User Experience
On Submit
Form submission
Validate all fields in submit handler
Less intrusive, batch errors
On Blur
Field loses focus
Validate in onBlur handler
Immediate feedback after editing
On Change
Every keystroke
Validate in onChange handler
Real-time feedback (can be annoying)
Debounced
After pause in typing
useDebounce + validation
Balance between immediate and batch
Mixed
Different rules per field
Combine strategies
Optimal UX per field type
// Validation on submit
const SubmitValidation = () => {
const [ formData , setFormData ] = useState ({ email: '' , password: '' });
const [ errors , setErrors ] = useState ({});
const validate = () => {
const newErrors = {};
if ( ! formData.email) {
newErrors.email = 'Email is required' ;
} else if ( ! / \S + @ \S + \. \S + / . test (formData.email)) {
newErrors.email = 'Email is invalid' ;
}
if ( ! formData.password) {
newErrors.password = 'Password is required' ;
} else if (formData.password. length < 8 ) {
newErrors.password = 'Password must be at least 8 characters' ;
}
return newErrors;
};
const handleSubmit = ( e ) => {
e. preventDefault ();
const validationErrors = validate ();
if (Object. keys (validationErrors). length > 0 ) {
setErrors (validationErrors);
return ;
}
// Submit form
console. log ( 'Valid form data:' , formData);
setErrors ({});
};
const handleChange = ( e ) => {
const { name , value } = e.target;
setFormData ( prev => ({ ... prev, [name]: value }));
// Clear error when user starts typing
if (errors[name]) {
setErrors ( prev => ({ ... prev, [name]: '' }));
}
};
return (
< form onSubmit = {handleSubmit}>
< div >
< input
name = "email"
value = {formData.email}
onChange = {handleChange}
placeholder = "Email"
/>
{errors.email && < span className = "error" >{errors.email}</ span >}
</ div >
< div >
< input
name = "password"
type = "password"
value = {formData.password}
onChange = {handleChange}
placeholder = "Password"
/>
{errors.password && < span className = "error" >{errors.password}</ span >}
</ div >
< button type = "submit" >Submit</ button >
</ form >
);
};
// Validation on blur (after editing)
const BlurValidation = () => {
const [ email , setEmail ] = useState ( '' );
const [ touched , setTouched ] = useState ( false );
const [ error , setError ] = useState ( '' );
const validateEmail = ( value ) => {
if ( ! value) return 'Email is required' ;
if ( ! / \S + @ \S + \. \S + / . test (value)) return 'Email is invalid' ;
return '' ;
};
const handleBlur = () => {
setTouched ( true );
setError ( validateEmail (email));
};
return (
< div >
< input
value = {email}
onChange = {( e ) => {
setEmail (e.target.value);
if (touched) setError ( validateEmail (e.target.value));
}}
onBlur = {handleBlur}
placeholder = "Email"
/>
{touched && error && < span className = "error" >{error}</ span >}
</ div >
);
};
// Custom validation hook
const useFormValidation = ( initialState , validationRules ) => {
const [ values , setValues ] = useState (initialState);
const [ errors , setErrors ] = useState ({});
const [ touched , setTouched ] = useState ({});
const validate = ( fieldName , value ) => {
const rule = validationRules[fieldName];
if ( ! rule) return '' ;
for ( const validator of rule) {
const error = validator (value, values);
if (error) return error;
}
return '' ;
};
const handleChange = ( e ) => {
const { name , value } = e.target;
setValues ( prev => ({ ... prev, [name]: value }));
if (touched[name]) {
const error = validate (name, value);
setErrors ( prev => ({ ... prev, [name]: error }));
}
};
const handleBlur = ( e ) => {
const { name } = e.target;
setTouched ( prev => ({ ... prev, [name]: true }));
const error = validate (name, values[name]);
setErrors ( prev => ({ ... prev, [name]: error }));
};
const validateAll = () => {
const newErrors = {};
Object. keys (validationRules). forEach ( field => {
const error = validate (field, values[field]);
if (error) newErrors[field] = error;
});
setErrors (newErrors);
return Object. keys (newErrors). length === 0 ;
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateAll
};
};
// Usage
const ValidationForm = () => {
const { values , errors , touched , handleChange , handleBlur , validateAll } =
useFormValidation (
{ email: '' , password: '' },
{
email: [
( v ) => ! v ? 'Required' : '' ,
( v ) => ! / \S + @ \S + \. \S + / . test (v) ? 'Invalid email' : ''
],
password: [
( v ) => ! v ? 'Required' : '' ,
( v ) => v. length < 8 ? 'Min 8 characters' : ''
]
}
);
const handleSubmit = ( e ) => {
e. preventDefault ();
if ( validateAll ()) {
console. log ( 'Valid:' , values);
}
};
return (
< form onSubmit = {handleSubmit}>
< input
name = "email"
value = {values.email}
onChange = {handleChange}
onBlur = {handleBlur}
/>
{touched.email && errors.email && < span >{errors.email}</ span >}
< button type = "submit" >Submit</ button >
</ form >
);
};
Pattern
Purpose
Benefits
Example
Wrapper Components
Consistent styling and behavior
Reusability, DRY, branding
Custom TextInput with label/error
useInput Hook
Extract input state logic
Cleaner components, reusable logic
Hook returning value, onChange, reset
useForm Hook
Manage entire form state
Centralized form logic
Hook with values, errors, handlers
Compound Components
Flexible input structure
Composable API, flexibility
Input.Label, Input.Field, Input.Error
Controlled Wrappers
Abstract third-party inputs
Consistent API, testability
Wrap date picker with standard props
// Custom input hook
const useInput = ( initialValue = '' , validation ) => {
const [ value , setValue ] = useState (initialValue);
const [ error , setError ] = useState ( '' );
const [ touched , setTouched ] = useState ( false );
const onChange = ( e ) => {
const newValue = e.target.value;
setValue (newValue);
if (touched && validation) {
setError ( validation (newValue));
}
};
const onBlur = () => {
setTouched ( true );
if (validation) {
setError ( validation (value));
}
};
const reset = () => {
setValue (initialValue);
setError ( '' );
setTouched ( false );
};
return {
value,
error,
touched,
onChange,
onBlur,
reset,
bind: { value, onChange, onBlur }
};
};
// Usage
const LoginForm = () => {
const email = useInput ( '' , ( v ) =>
! v ? 'Required' : ! / \S + @ \S + / . test (v) ? 'Invalid' : ''
);
const password = useInput ( '' , ( v ) =>
v. length < 8 ? 'Min 8 chars' : ''
);
const handleSubmit = ( e ) => {
e. preventDefault ();
if ( ! email.error && ! password.error) {
console. log ({ email: email.value, password: password.value });
}
};
return (
< form onSubmit = {handleSubmit}>
< input { ... email.bind} placeholder = "Email" />
{email.touched && email.error && < span >{email.error}</ span >}
< input { ... password.bind} type = "password" placeholder = "Password" />
{password.touched && password.error && < span >{password.error}</ span >}
< button type = "submit" >Login</ button >
</ form >
);
};
// Custom input wrapper component
const TextField = ({ label , error , touched , ... props }) => {
return (
< div className = "field" >
{label && < label >{label}</ label >}
< input { ... props} className = {error && touched ? 'error' : '' } />
{touched && error && (
< span className = "error-message" >{error}</ span >
)}
</ div >
);
};
// Usage
const FormWithTextField = () => {
const email = useInput ( '' );
return (
< TextField
label = "Email"
type = "email"
error = {email.error}
touched = {email.touched}
{ ... email.bind}
/>
);
};
// Compound component pattern
const Input = ({ children , error , touched }) => {
return (
< div className = "input-group" >
{children}
{touched && error && < Input.Error >{error}</ Input.Error >}
</ div >
);
};
Input. Label = ({ children , htmlFor }) => (
< label htmlFor = {htmlFor} className = "input-label" >{children}</ label >
);
Input. Field = ({ id , ... props }) => (
< input id = {id} className = "input-field" { ... props} />
);
Input. Error = ({ children }) => (
< span className = "input-error" >{children}</ span >
);
// Usage
const CompoundForm = () => {
const email = useInput ( '' );
return (
< Input error = {email.error} touched = {email.touched}>
< Input.Label htmlFor = "email" >Email</ Input.Label >
< Input.Field id = "email" type = "email" { ... email.bind} />
</ Input >
);
};
// Custom form hook
const useForm = ( initialValues , validationSchema ) => {
const [ values , setValues ] = useState (initialValues);
const [ errors , setErrors ] = useState ({});
const [ touched , setTouched ] = useState ({});
const [ isSubmitting , setIsSubmitting ] = useState ( false );
const validateField = ( name , value ) => {
if (validationSchema[name]) {
return validationSchema[name](value, values);
}
return '' ;
};
const handleChange = ( e ) => {
const { name , value } = e.target;
setValues ( prev => ({ ... prev, [name]: value }));
if (touched[name]) {
const error = validateField (name, value);
setErrors ( prev => ({ ... prev, [name]: error }));
}
};
const handleBlur = ( e ) => {
const { name } = e.target;
setTouched ( prev => ({ ... prev, [name]: true }));
const error = validateField (name, values[name]);
setErrors ( prev => ({ ... prev, [name]: error }));
};
const handleSubmit = async ( onSubmit ) => {
return async ( e ) => {
e. preventDefault ();
// Validate all fields
const newErrors = {};
Object. keys (validationSchema). forEach ( field => {
const error = validateField (field, values[field]);
if (error) newErrors[field] = error;
});
setErrors (newErrors);
setTouched (Object. keys (validationSchema). reduce (
( acc , key ) => ({ ... acc, [key]: true }), {}
));
if (Object. keys (newErrors). length === 0 ) {
setIsSubmitting ( true );
try {
await onSubmit (values);
} finally {
setIsSubmitting ( false );
}
}
};
};
const reset = () => {
setValues (initialValues);
setErrors ({});
setTouched ({});
};
return {
values,
errors,
touched,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
reset
};
};
Library
Philosophy
Pros
Cons
Formik
Component-based, render props
Easy to learn, good DX, field-level validation
More re-renders, larger bundle
React Hook Form
Hook-based, uncontrolled inputs
High performance, small bundle, less re-renders
Steeper learning curve, ref-based
Final Form
Framework-agnostic core
Flexible, powerful, subscription-based
More complex API
Yup / Zod
Schema validation libraries
Type-safe, reusable schemas, great DX
Additional dependency
// FORMIK Example
import { useFormik } from 'formik' ;
import * as Yup from 'yup' ;
const FormikForm = () => {
const formik = useFormik ({
initialValues: {
email: '' ,
password: ''
},
validationSchema: Yup. object ({
email: Yup. string ()
. email ( 'Invalid email' )
. required ( 'Required' ),
password: Yup. string ()
. min ( 8 , 'Must be 8 characters or more' )
. required ( 'Required' )
}),
onSubmit : async ( values ) => {
await api. login (values);
}
});
return (
< form onSubmit = {formik.handleSubmit}>
< input
name = "email"
type = "email"
onChange = {formik.handleChange}
onBlur = {formik.handleBlur}
value = {formik.values.email}
/>
{formik.touched.email && formik.errors.email && (
< div >{formik.errors.email}</ div >
)}
< input
name = "password"
type = "password"
onChange = {formik.handleChange}
onBlur = {formik.handleBlur}
value = {formik.values.password}
/>
{formik.touched.password && formik.errors.password && (
< div >{formik.errors.password}</ div >
)}
< button type = "submit" disabled = {formik.isSubmitting}>
Submit
</ button >
</ form >
);
};
// Formik with Formik components
import { Formik, Form, Field, ErrorMessage } from 'formik' ;
const FormikComponents = () => (
< Formik
initialValues = {{ email: '' , password: '' }}
validationSchema = {validationSchema}
onSubmit = { async ( values ) => {
await api. login (values);
}}
>
{({ isSubmitting }) => (
< Form >
< Field name = "email" type = "email" />
< ErrorMessage name = "email" component = "div" />
< Field name = "password" type = "password" />
< ErrorMessage name = "password" component = "div" />
< button type = "submit" disabled = {isSubmitting}>
Submit
</ button >
</ Form >
)}
</ Formik >
);
// REACT HOOK FORM Example
import { useForm } from 'react-hook-form' ;
import { zodResolver } from '@hookform/resolvers/zod' ;
import { z } from 'zod' ;
const schema = z. object ({
email: z. string (). email ( 'Invalid email' ). min ( 1 , 'Required' ),
password: z. string (). min ( 8 , 'Must be at least 8 characters' )
});
const ReactHookForm = () => {
const {
register ,
handleSubmit ,
formState : { errors , isSubmitting }
} = useForm ({
resolver: zodResolver (schema)
});
const onSubmit = async ( data ) => {
await api. login (data);
};
return (
< form onSubmit = { handleSubmit (onSubmit)}>
< input
{ ... register ( 'email' )}
type = "email"
placeholder = "Email"
/>
{errors.email && < span >{errors.email.message}</ span >}
< input
{ ... register ( 'password' )}
type = "password"
placeholder = "Password"
/>
{errors.password && < span >{errors.password.message}</ span >}
< button type = "submit" disabled = {isSubmitting}>
Submit
</ button >
</ form >
);
};
// React Hook Form with Controller (for custom components)
import { Controller } from 'react-hook-form' ;
const RHFWithController = () => {
const { control , handleSubmit } = useForm ();
return (
< form onSubmit = { handleSubmit (onSubmit)}>
< Controller
name = "customInput"
control = {control}
rules = {{ required: 'This field is required' }}
render = {({ field , fieldState }) => (
< div >
< CustomInput { ... field} />
{fieldState.error && < span >{fieldState.error.message}</ span >}
</ div >
)}
/>
< button type = "submit" >Submit</ button >
</ form >
);
};
Note: React Hook Form is generally recommended for performance-critical apps due to fewer
re-renders.
Formik offers easier learning curve and better integration with controlled components.
Feature
Implementation
Key Points
Browser API
File Input
<input type="file" ref={fileRef} />
Always uncontrolled
Access via ref.current.files
Multiple Files
<input type="file" multiple />
files is FileList (array-like)
Array.from(files)
File Preview
URL.createObjectURL(file)
Create temporary URL
Revoke with URL.revokeObjectURL
FormData
new FormData()
Build multipart/form-data
Append files and fields
Drag and Drop
onDrop, onDragOver handlers
e.dataTransfer.files
preventDefault() required
Progress
XMLHttpRequest or axios
onUploadProgress callback
Track upload percentage
// Basic file upload
const FileUpload = () => {
const [ file , setFile ] = useState ( null );
const [ preview , setPreview ] = useState ( null );
const handleFileChange = ( e ) => {
const selectedFile = e.target.files[ 0 ];
if (selectedFile) {
setFile (selectedFile);
// Create preview URL for images
if (selectedFile.type. startsWith ( 'image/' )) {
const url = URL . createObjectURL (selectedFile);
setPreview (url);
}
}
};
const handleUpload = async () => {
if ( ! file) return ;
const formData = new FormData ();
formData. append ( 'file' , file);
formData. append ( 'description' , 'My file' );
try {
const response = await fetch ( '/api/upload' , {
method: 'POST' ,
body: formData
});
const data = await response. json ();
console. log ( 'Upload success:' , data);
} catch (error) {
console. error ( 'Upload failed:' , error);
}
};
// Cleanup preview URL
useEffect (() => {
return () => {
if (preview) URL . revokeObjectURL (preview);
};
}, [preview]);
return (
< div >
< input type = "file" onChange = {handleFileChange} accept = "image/*" />
{preview && < img src = {preview} alt = "Preview" style = {{ width: 200 }} />}
{file && (
< div >
< p >File: {file.name}</ p >
< p >Size: {(file.size / 1024 ). toFixed ( 2 )} KB</ p >
< button onClick = {handleUpload}>Upload</ button >
</ div >
)}
</ div >
);
};
// Multiple file upload
const MultiFileUpload = () => {
const [ files , setFiles ] = useState ([]);
const handleFileChange = ( e ) => {
const fileList = Array. from (e.target.files);
setFiles (fileList);
};
const handleUpload = async () => {
const formData = new FormData ();
files. forEach (( file , index ) => {
formData. append (\ `files[ \$ {index}] \` , file);
});
await fetch('/api/upload-multiple', {
method: 'POST',
body: formData
});
};
return (
<div>
<input type="file" multiple onChange={handleFileChange} />
<ul>
{files.map((file, index) => (
<li key={index}>{file.name} - {file.size} bytes</li>
))}
</ul>
{files.length > 0 && (
<button onClick={handleUpload}>Upload All</button>
)}
</div>
);
};
// Drag and drop file upload
const DragDropUpload = () => {
const [files, setFiles] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const handleDragOver = (e) => {
e.preventDefault();
setIsDragging(true);
};
const handleDragLeave = () => {
setIsDragging(false);
};
const handleDrop = (e) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
setFiles(prev => [...prev, ...droppedFiles]);
};
return (
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
style={{
border: isDragging ? '2px solid blue' : '2px dashed gray',
padding: '2rem',
textAlign: 'center'
}}
>
{files.length === 0 ? (
<p>Drag and drop files here</p>
) : (
<ul>
{files.map((file, i) => (
<li key={i}>{file.name}</li>
))}
</ul>
)}
</div>
);
};
// Upload with progress
const UploadWithProgress = () => {
const [file, setFile] = useState(null);
const [progress, setProgress] = useState(0);
const [uploading, setUploading] = useState(false);
const handleUpload = async () => {
if (!file) return;
setUploading(true);
const formData = new FormData();
formData.append('file', file);
try {
// Using axios for upload progress
const response = await axios.post('/api/upload', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
setProgress(percentCompleted);
}
});
console.log('Upload complete:', response.data);
} catch (error) {
console.error('Upload failed:', error);
} finally {
setUploading(false);
setProgress(0);
}
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files[0])}
disabled={uploading}
/>
{uploading && (
<div>
<progress value={progress} max="100" />
<span>{progress}%</span>
</div>
)}
{file && !uploading && (
<button onClick={handleUpload}>Upload</button>
)}
</div>
);
};
// Complete form with file and other fields
const CompleteForm = () => {
const [formData, setFormData] = useState({
title: '',
description: '',
file: null
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleFileChange = (e) => {
setFormData(prev => ({ ...prev, file: e.target.files[0] }));
};
const handleSubmit = async (e) => {
e.preventDefault();
const data = new FormData();
data.append('title', formData.title);
data.append('description', formData.description);
if (formData.file) {
data.append('file', formData.file);
}
await fetch('/api/submit', {
method: 'POST',
body: data
});
};
return (
<form onSubmit={handleSubmit}>
<input
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Title"
/>
<textarea
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Description"
/>
<input type="file" onChange={handleFileChange} />
<button type="submit">Submit</button>
</form>
);
};
Warning: Always validate file types and sizes on the server. Client-side validation (accept
attribute)
is only for UX. Remember to revoke object URLs with URL.revokeObjectURL() to prevent memory leaks.
Controlled components : Prefer for most inputs - React state as single
source of truth
Validation timing : Validate on blur for better UX, on submit as final check
Form libraries : Use React Hook Form for performance, Formik for easier DX
Custom hooks : Extract form logic for reusability and cleaner components
File uploads : Always uncontrolled, use FormData, validate server-side
Error messages : Show after touched, clear on change, batch on submit
7. Event Handling and User Interactions
7.1 Event Handler Syntax and Event Objects
Syntax
Pattern
Description
Use Case
Inline Arrow
onClick={() => handler()}
Arrow function in JSX
Pass arguments, simple calls
Function Reference
onClick={handler}
Direct function reference
Better performance, no args
Event Object
(e) => handler(e)
Access event properties
Get target value, prevent default
With Parameters
onClick={(e) => handler(id, e)}
Pass custom args with event
Item actions, list operations
Bind Method
onClick={this.handler.bind(this, id)}
Class component binding
Legacy class components
Prevent Default
e.preventDefault()
Stop default browser action
Forms, links, context menu
Stop Propagation
e.stopPropagation()
Stop event bubbling
Nested clickable elements
Example: Event handler patterns
// Basic event handlers
const EventHandlers = () => {
const [ count , setCount ] = useState ( 0 );
// Simple handler
const handleClick = () => {
setCount ( c => c + 1 );
};
// Handler with event object
const handleChange = ( e ) => {
console. log ( 'Input value:' , e.target.value);
};
// Handler with custom parameters
const handleDelete = ( id ) => {
console. log ( 'Delete item:' , id);
};
// Handler with both custom params and event
const handleEdit = ( id , e ) => {
e. stopPropagation (); // Stop bubbling
console. log ( 'Edit item:' , id);
};
return (
< div >
{ /* Function reference (preferred for simple cases) */ }
< button onClick = {handleClick}>Count: {count}</ button >
{ /* Inline arrow function (needed for params) */ }
< button onClick = {() => handleDelete ( 123 )}>Delete</ button >
{ /* With event and params */ }
< button onClick = {( e ) => handleEdit ( 456 , e)}>Edit</ button >
{ /* Event object access */ }
< input onChange = {handleChange} />
</ div >
);
};
// Prevent default behavior
const FormWithLink = () => {
const handleSubmit = ( e ) => {
e. preventDefault (); // Don't reload page
console. log ( 'Form submitted' );
};
const handleLinkClick = ( e ) => {
e. preventDefault (); // Don't navigate
console. log ( 'Handle navigation in React' );
};
return (
< div >
< form onSubmit = {handleSubmit}>
< input type = "text" />
< button type = "submit" >Submit</ button >
</ form >
< a href = "/page" onClick = {handleLinkClick}>
Custom navigation
</ a >
</ div >
);
};
// Stop propagation for nested elements
const NestedClickables = () => {
const handleParentClick = () => {
console. log ( 'Parent clicked' );
};
const handleChildClick = ( e ) => {
e. stopPropagation (); // Don't trigger parent
console. log ( 'Child clicked' );
};
return (
< div onClick = {handleParentClick} style = {{ padding: '2rem' , border: '1px solid' }}>
Parent
< button onClick = {handleChildClick}>
Child (won't trigger parent)
</ button >
</ div >
);
};
// Event object properties
const EventProperties = () => {
const handleEvent = ( e ) => {
console. log ( 'Event type:' , e.type);
console. log ( 'Target element:' , e.target);
console. log ( 'Current target:' , e.currentTarget);
console. log ( 'Key pressed:' , e.key);
console. log ( 'Mouse position:' , e.clientX, e.clientY);
console. log ( 'Modifier keys:' , {
ctrl: e.ctrlKey,
shift: e.shiftKey,
alt: e.altKey,
meta: e.metaKey
});
};
return (
< div >
< button onClick = {handleEvent}>Click me</ button >
< input onKeyDown = {handleEvent} />
</ div >
);
};
Note: Use function references when possible for better performance. Use inline arrow functions
only
when you need to pass arguments or access closure variables.
7.2 Synthetic Events and Cross-browser Compatibility
Feature
Description
Benefits
Important Notes
Synthetic Events
React's wrapper around native events
Cross-browser consistency
Same API across all browsers
Event Pooling (Legacy)
Events reused for performance
Memory efficiency
Removed in React 17+
nativeEvent
e.nativeEvent access
Access underlying browser event
Use only when necessary
Event Delegation
Events attached to root (React 16)
Performance, memory efficiency
Changed in React 17 (root container)
Passive Events
Non-blocking scroll/touch events
Better scroll performance
Can't preventDefault() on passive
Example: Synthetic events and cross-browser handling
// Synthetic event properties
const SyntheticEventDemo = () => {
const handleClick = ( e ) => {
// Synthetic event properties (same across browsers)
console. log ( 'Synthetic event:' , e);
console. log ( 'Type:' , e.type);
console. log ( 'Target:' , e.target);
console. log ( 'Timestamp:' , e.timeStamp);
// Access native browser event if needed
console. log ( 'Native event:' , e.nativeEvent);
// Common methods work consistently
e. preventDefault ();
e. stopPropagation ();
};
return < button onClick = {handleClick}>Click</ button >;
};
// No need to worry about event pooling in React 17+
const ModernEventHandling = () => {
const handleClick = ( e ) => {
// In React 17+, can access event properties asynchronously
setTimeout (() => {
console. log ( 'Event type:' , e.type); // Works!
console. log ( 'Target:' , e.target); // Works!
}, 1000 );
};
return < button onClick = {handleClick}>Click</ button >;
};
// Legacy React (<17) required persist() for async access
const LegacyAsyncEventAccess = () => {
const handleClick = ( e ) => {
e. persist (); // No longer needed in React 17+
setTimeout (() => {
console. log ( 'Event type:' , e.type);
}, 1000 );
};
return < button onClick = {handleClick}>Click</ button >;
};
// Native event access for special cases
const NativeEventAccess = () => {
const handleWheel = ( e ) => {
// Synthetic event
console. log ( 'Delta Y:' , e.deltaY);
// Native event for browser-specific features
const nativeEvent = e.nativeEvent;
console. log ( 'Native wheel event:' , nativeEvent);
};
return (
< div
onWheel = {handleWheel}
style = {{ height: '200px' , overflow: 'auto' }}
>
Scroll me
</ div >
);
};
// Cross-browser event handling
const CrossBrowserEvents = () => {
const handleInput = ( e ) => {
// Works consistently across all browsers
const value = e.target.value;
console. log ( 'Input value:' , value);
};
const handlePaste = ( e ) => {
// Clipboard access works consistently
const pastedText = e.clipboardData. getData ( 'text' );
console. log ( 'Pasted:' , pastedText);
};
const handleDrag = ( e ) => {
// Drag events normalized
console. log ( 'Dragging:' , e.dataTransfer);
};
return (
< div >
< input onChange = {handleInput} onPaste = {handlePaste} />
< div draggable onDrag = {handleDrag}>Drag me</ div >
</ div >
);
};
// Event delegation (automatic in React)
const EventDelegationExample = () => {
// React automatically delegates events to root
// No need to manually attach/remove listeners
const handleClick = ( id ) => {
console. log ( 'Clicked item:' , id);
};
return (
< ul >
{[ 1 , 2 , 3 , 4 , 5 ]. map ( id => (
< li key = {id} onClick = {() => handleClick (id)}>
Item {id}
</ li >
))}
</ ul >
);
// React attaches ONE listener at root, not 5 listeners
};
Note: React's synthetic events provide consistent behavior across browsers. Event pooling was
removed
in React 17+, so you can safely access event properties asynchronously without calling persist().
Pattern
Performance
When to Use
Example
Inline Arrow
Creates new function each render
Simple cases, rare renders
onClick={() => handler()}
useCallback
Memoized function reference
Optimize child re-renders
const cb = useCallback(() => {}, [])
Function Reference
Best - stable reference
No arguments needed
onClick={handler}
Closure Factory
Creates function per item
List items with IDs
const makeHandler = (id) => () => {}
Data Attributes
Single handler, read from event
Lists with simple actions
data-id={id} then read in handler
Example: Event handler optimization
// ❌ Bad: Inline arrow in child with memo
const Child = memo (({ onClick }) => {
console. log ( 'Child rendered' );
return < button onClick = {onClick}>Click</ button >;
});
const BadParent = () => {
const [ count , setCount ] = useState ( 0 );
return (
< div >
{ /* Child re-renders every time because new function */ }
< Child onClick = {() => console. log ( 'clicked' )} />
< button onClick = {() => setCount ( c => c + 1 )}>{count}</ button >
</ div >
);
};
// ✓ Good: useCallback for stable reference
const GoodParent = () => {
const [ count , setCount ] = useState ( 0 );
const handleClick = useCallback (() => {
console. log ( 'clicked' );
}, []);
return (
< div >
{ /* Child won't re-render unnecessarily */ }
< Child onClick = {handleClick} />
< button onClick = {() => setCount ( c => c + 1 )}>{count}</ button >
</ div >
);
};
// ❌ Bad: Creating new function per list item
const BadList = () => {
const [ items , setItems ] = useState ([ 1 , 2 , 3 , 4 , 5 ]);
const handleDelete = ( id ) => {
setItems ( items => items. filter ( i => i !== id));
};
return (
< ul >
{items. map ( item => (
< li key = {item}>
{item}
{ /* New function created for each item */ }
< button onClick = {() => handleDelete (item)}>Delete</ button >
</ li >
))}
</ ul >
);
};
// ✓ Better: Data attributes pattern
const BetterList = () => {
const [ items , setItems ] = useState ([ 1 , 2 , 3 , 4 , 5 ]);
const handleDelete = ( e ) => {
const id = parseInt (e.currentTarget.dataset.id);
setItems ( items => items. filter ( i => i !== id));
};
return (
< ul >
{items. map ( item => (
< li key = {item}>
{item}
{ /* Single handler reference */ }
< button data-id = {item} onClick = {handleDelete}>Delete</ button >
</ li >
))}
</ ul >
);
};
// ✓ Good: Memoized list items
const ListItem = memo (({ item , onDelete }) => {
return (
< li >
{item}
< button onClick = {onDelete}>Delete</ button >
</ li >
);
});
const OptimizedList = () => {
const [ items , setItems ] = useState ([ 1 , 2 , 3 , 4 , 5 ]);
const handleDelete = useCallback (( id ) => {
setItems ( items => items. filter ( i => i !== id));
}, []);
return (
< ul >
{items. map ( item => (
< ListItem
key = {item}
item = {item}
onDelete = {() => handleDelete (item)}
/>
))}
</ ul >
);
};
// Performance optimization decision guide
const PerformanceGuide = () => {
// Simple component, no memo on children → inline is fine
const simple = () => (
< button onClick = {() => console. log ( 'click' )}>Click</ button >
);
// Memo'ed children → use useCallback
const [ count , setCount ] = useState ( 0 );
const optimized = useCallback (() => {
console. log ( 'click' );
}, []);
// Large lists → use data attributes or memoized items
// Rare updates → inline is fine
// Frequent updates → optimize
};
Warning: Don't prematurely optimize! Inline arrow functions are fine for most cases. Only use
useCallback when passing handlers to memoized children or in large lists with frequent updates.
7.4 Custom Event Handlers and Event Composition
Pattern
Purpose
Implementation
Use Case
Event Composition
Chain multiple handlers
Call multiple functions in sequence
Logging + action
Higher-Order Handlers
Create handlers with common logic
Function that returns handler
Tracking, validation
Custom Events
Component-specific events
Custom event callbacks via props
onSuccess, onError callbacks
Event Middlewares
Transform or validate events
Wrapper function around handler
Auth checks, rate limiting
Debounced/Throttled
Control event frequency
useDebounce/useThrottle hooks
Search input, scroll, resize
Example: Custom event handlers
// Event composition
const EventComposition = () => {
const logEvent = ( eventName ) => {
console. log (\ `Event: \$ {eventName} \` );
};
const trackEvent = (eventName) => {
analytics.track(eventName);
};
const handleClick = () => {
console.log('Button clicked');
};
// Compose multiple handlers
const composedHandler = (e) => {
logEvent('button-click');
trackEvent('button-click');
handleClick();
};
return <button onClick={composedHandler}>Click</button>;
};
// Higher-order handler
const withTracking = (handler, eventName) => {
return (e) => {
analytics.track(eventName);
handler(e);
};
};
const withValidation = (handler, validate) => {
return (e) => {
if (validate(e)) {
handler(e);
}
};
};
const TrackedButton = () => {
const handleClick = () => {
console.log('Clicked');
};
const trackedHandler = withTracking(handleClick, 'button-clicked');
return <button onClick={trackedHandler}>Click</button>;
};
// Custom events via props
const FileUploader = ({ onUploadStart, onUploadSuccess, onUploadError }) => {
const [uploading, setUploading] = useState(false);
const handleUpload = async (file) => {
setUploading(true);
onUploadStart?.();
try {
const result = await uploadFile(file);
onUploadSuccess?.(result);
} catch (error) {
onUploadError?.(error);
} finally {
setUploading(false);
}
};
return (
<input
type="file"
onChange={(e) => handleUpload(e.target.files[0])}
disabled={uploading}
/>
);
};
// Usage
const App = () => (
<FileUploader
onUploadStart={() => console.log('Starting...')}
onUploadSuccess={(data) => console.log('Success:', data)}
onUploadError={(err) => console.error('Error:', err)}
/>
);
// Debounced event handler
const useDebounce = (callback, delay) => {
const timeoutRef = useRef(null);
return useCallback((...args) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
}, [callback, delay]);
};
const SearchInput = () => {
const [query, setQuery] = useState('');
const performSearch = (searchTerm) => {
console.log('Searching for:', searchTerm);
// API call here
};
const debouncedSearch = useDebounce(performSearch, 500);
const handleChange = (e) => {
const value = e.target.value;
setQuery(value);
debouncedSearch(value);
};
return <input value={query} onChange={handleChange} />;
};
// Throttled event handler
const useThrottle = (callback, delay) => {
const lastRun = useRef(Date.now());
return useCallback((...args) => {
const now = Date.now();
if (now - lastRun.current >= delay) {
callback(...args);
lastRun.current = now;
}
}, [callback, delay]);
};
const ScrollTracker = () => {
const trackScroll = (scrollY) => {
console.log('Scroll position:', scrollY);
};
const throttledTrack = useThrottle(() => {
trackScroll(window.scrollY);
}, 1000);
useEffect(() => {
window.addEventListener('scroll', throttledTrack);
return () => window.removeEventListener('scroll', throttledTrack);
}, [throttledTrack]);
return <div>Scroll the page</div>;
};
// Event middleware pattern
const withAuth = (handler) => {
return (e) => {
const isAuthenticated = checkAuth();
if (!isAuthenticated) {
alert('Please log in');
return;
}
handler(e);
};
};
const ProtectedButton = () => {
const handleAction = () => {
console.log('Performing protected action');
};
return (
<button onClick={withAuth(handleAction)}>
Protected Action
</button>
);
};
7.5 Keyboard and Mouse Event Patterns
Event Type
Event Name
Common Properties
Use Case
Keyboard
onKeyDown, onKeyUp, onKeyPress
key, code, keyCode, ctrlKey, shiftKey
Shortcuts, form navigation, games
Mouse Click
onClick, onDoubleClick, onContextMenu
button, clientX, clientY
Actions, menus, selection
Mouse Movement
onMouseMove, onMouseEnter, onMouseLeave
clientX, clientY, movementX, movementY
Tooltips, drag, hover effects
Mouse Drag
onMouseDown, onMouseMove, onMouseUp
Track position changes
Draggable elements, drawing
Wheel
onWheel
deltaX, deltaY, deltaZ
Custom scrolling, zoom
Example: Keyboard and mouse events
// Keyboard shortcuts
const KeyboardShortcuts = () => {
const handleKeyDown = ( e ) => {
// Check for specific key
if (e.key === 'Enter' ) {
console. log ( 'Enter pressed' );
}
// Keyboard shortcuts with modifiers
if (e.ctrlKey && e.key === 's' ) {
e. preventDefault ();
console. log ( 'Save (Ctrl+S)' );
}
if (e.ctrlKey && e.shiftKey && e.key === 'P' ) {
e. preventDefault ();
console. log ( 'Open command palette (Ctrl+Shift+P)' );
}
// Arrow keys
switch (e.key) {
case 'ArrowUp' :
console. log ( 'Up' );
break ;
case 'ArrowDown' :
console. log ( 'Down' );
break ;
case 'ArrowLeft' :
console. log ( 'Left' );
break ;
case 'ArrowRight' :
console. log ( 'Right' );
break ;
}
};
return (
< div onKeyDown = {handleKeyDown} tabIndex = { 0 }>
Press keys (Ctrl+S, Arrows, Enter)
</ div >
);
};
// Key codes reference
const KeyboardReference = () => {
const handleKeyDown = ( e ) => {
console. log ( 'Key:' , e.key); // 'a', 'Enter', 'ArrowUp'
console. log ( 'Code:' , e.code); // 'KeyA', 'Enter', 'ArrowUp'
console. log ( 'KeyCode:' , e.keyCode); // 65, 13, 38 (deprecated)
console. log ( 'Modifiers:' , {
ctrl: e.ctrlKey,
shift: e.shiftKey,
alt: e.altKey,
meta: e.metaKey // Cmd on Mac, Win on Windows
});
};
return < input onKeyDown = {handleKeyDown} />;
};
// Mouse events
const MouseEvents = () => {
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
const [ isHovering , setIsHovering ] = useState ( false );
const handleMouseMove = ( e ) => {
setPosition ({ x: e.clientX, y: e.clientY });
};
const handleClick = ( e ) => {
console. log ( 'Click at:' , e.clientX, e.clientY);
console. log ( 'Button:' , e.button); // 0=left, 1=middle, 2=right
};
const handleDoubleClick = () => {
console. log ( 'Double clicked' );
};
const handleContextMenu = ( e ) => {
e. preventDefault (); // Prevent default context menu
console. log ( 'Right clicked' );
};
return (
< div
onMouseMove = {handleMouseMove}
onClick = {handleClick}
onDoubleClick = {handleDoubleClick}
onContextMenu = {handleContextMenu}
onMouseEnter = {() => setIsHovering ( true )}
onMouseLeave = {() => setIsHovering ( false )}
style = {{
width: '100%' ,
height: '300px' ,
border: '1px solid' ,
backgroundColor: isHovering ? '#f0f0f0' : 'white'
}}
>
Mouse position: {position.x}, {position.y}
{isHovering && ' (Hovering)' }
</ div >
);
};
// Draggable element
const DraggableBox = () => {
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
const [ isDragging , setIsDragging ] = useState ( false );
const [ dragStart , setDragStart ] = useState ({ x: 0 , y: 0 });
const handleMouseDown = ( e ) => {
setIsDragging ( true );
setDragStart ({
x: e.clientX - position.x,
y: e.clientY - position.y
});
};
const handleMouseMove = ( e ) => {
if (isDragging) {
setPosition ({
x: e.clientX - dragStart.x,
y: e.clientY - dragStart.y
});
}
};
const handleMouseUp = () => {
setIsDragging ( false );
};
useEffect (() => {
if (isDragging) {
window. addEventListener ( 'mousemove' , handleMouseMove);
window. addEventListener ( 'mouseup' , handleMouseUp);
return () => {
window. removeEventListener ( 'mousemove' , handleMouseMove);
window. removeEventListener ( 'mouseup' , handleMouseUp);
};
}
}, [isDragging, dragStart]);
return (
< div
onMouseDown = {handleMouseDown}
style = {{
position: 'absolute' ,
left: position.x,
top: position.y,
width: '100px' ,
height: '100px' ,
backgroundColor: 'blue' ,
cursor: isDragging ? 'grabbing' : 'grab'
}}
>
Drag me
</ div >
);
};
// Hover tooltip
const HoverTooltip = ({ children , tooltip }) => {
const [ show , setShow ] = useState ( false );
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
const handleMouseEnter = ( e ) => {
setShow ( true );
setPosition ({ x: e.clientX, y: e.clientY });
};
const handleMouseMove = ( e ) => {
setPosition ({ x: e.clientX, y: e.clientY });
};
return (
<>
< span
onMouseEnter = {handleMouseEnter}
onMouseMove = {handleMouseMove}
onMouseLeave = {() => setShow ( false )}
>
{children}
</ span >
{show && (
< div
style = {{
position: 'fixed' ,
left: position.x + 10 ,
top: position.y + 10 ,
background: 'black' ,
color: 'white' ,
padding: '5px' ,
borderRadius: '3px'
}}
>
{tooltip}
</ div >
)}
</>
);
};
7.6 Touch Events and Mobile Interaction Handling
Event
Description
Properties
Use Case
onTouchStart
Touch begins
touches, targetTouches, changedTouches
Detect touch, start gesture
onTouchMove
Touch moves
Touch coordinates, track movement
Swipe, drag, pan
onTouchEnd
Touch ends
Remaining touches
Complete gesture, trigger action
onTouchCancel
Touch interrupted
System interruption
Reset gesture state
Multi-touch
Multiple fingers
touches array
Pinch zoom, rotate
Gestures
Swipe, pinch, rotate
Calculate from touch positions
Mobile interactions
Example: Touch events and mobile gestures
// Basic touch events
const TouchDemo = () => {
const [ touchInfo , setTouchInfo ] = useState ( '' );
const handleTouchStart = ( e ) => {
const touch = e.touches[ 0 ];
setTouchInfo (\ `Touch started at \$ {touch.clientX}, \$ {touch.clientY} \` );
};
const handleTouchMove = (e) => {
const touch = e.touches[0];
setTouchInfo( \` Moving at \$ {touch.clientX}, \$ {touch.clientY} \` );
};
const handleTouchEnd = () => {
setTouchInfo('Touch ended');
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: '100%',
height: '200px',
border: '1px solid',
touchAction: 'none' // Prevent default touch behavior
}}
>
{touchInfo || 'Touch here'}
</div>
);
};
// Swipe detection
const SwipeDetector = ({ onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown }) => {
const [touchStart, setTouchStart] = useState(null);
const handleTouchStart = (e) => {
const touch = e.touches[0];
setTouchStart({ x: touch.clientX, y: touch.clientY });
};
const handleTouchEnd = (e) => {
if (!touchStart) return;
const touch = e.changedTouches[0];
const deltaX = touch.clientX - touchStart.x;
const deltaY = touch.clientY - touchStart.y;
const threshold = 50;
// Determine swipe direction
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Horizontal swipe
if (deltaX > threshold) {
onSwipeRight?.();
} else if (deltaX < -threshold) {
onSwipeLeft?.();
}
} else {
// Vertical swipe
if (deltaY > threshold) {
onSwipeDown?.();
} else if (deltaY < -threshold) {
onSwipeUp?.();
}
}
setTouchStart(null);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
style={{
width: '100%',
height: '300px',
border: '1px solid',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
Swipe in any direction
</div>
);
};
// Usage
const SwipeDemo = () => (
<SwipeDetector
onSwipeLeft={() => console.log('Swiped left')}
onSwipeRight={() => console.log('Swiped right')}
onSwipeUp={() => console.log('Swiped up')}
onSwipeDown={() => console.log('Swiped down')}
/>
);
// Pinch zoom
const PinchZoom = () => {
const [scale, setScale] = useState(1);
const [initialDistance, setInitialDistance] = useState(null);
const getDistance = (touch1, touch2) => {
const dx = touch1.clientX - touch2.clientX;
const dy = touch1.clientY - touch2.clientY;
return Math.sqrt(dx * dx + dy * dy);
};
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
const distance = getDistance(e.touches[0], e.touches[1]);
setInitialDistance(distance);
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2 && initialDistance) {
e.preventDefault();
const distance = getDistance(e.touches[0], e.touches[1]);
const newScale = (distance / initialDistance) * scale;
// Clamp scale between 0.5 and 3
setScale(Math.min(Math.max(newScale, 0.5), 3));
}
};
const handleTouchEnd = () => {
setInitialDistance(null);
};
return (
<div
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
style={{
width: '300px',
height: '300px',
border: '1px solid',
overflow: 'hidden',
touchAction: 'none'
}}
>
<div
style={{
transform: \` scale( \$ {scale}) \` ,
transformOrigin: 'center',
transition: initialDistance ? 'none' : 'transform 0.3s'
}}
>
Pinch to zoom (scale: {scale.toFixed(2)})
</div>
</div>
);
};
// Draggable with touch support
const TouchDraggable = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [startPos, setStartPos] = useState({ x: 0, y: 0 });
const handleStart = (clientX, clientY) => {
setIsDragging(true);
setStartPos({
x: clientX - position.x,
y: clientY - position.y
});
};
const handleMove = (clientX, clientY) => {
if (isDragging) {
setPosition({
x: clientX - startPos.x,
y: clientY - startPos.y
});
}
};
// Touch handlers
const handleTouchStart = (e) => {
const touch = e.touches[0];
handleStart(touch.clientX, touch.clientY);
};
const handleTouchMove = (e) => {
e.preventDefault();
const touch = e.touches[0];
handleMove(touch.clientX, touch.clientY);
};
// Mouse handlers (for desktop)
const handleMouseDown = (e) => {
handleStart(e.clientX, e.clientY);
};
const handleMouseMove = (e) => {
handleMove(e.clientX, e.clientY);
};
const handleEnd = () => {
setIsDragging(false);
};
useEffect(() => {
if (isDragging) {
window.addEventListener('mousemove', handleMouseMove);
window.addEventListener('mouseup', handleEnd);
return () => {
window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', handleEnd);
};
}
}, [isDragging]);
return (
<div
onMouseDown={handleMouseDown}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleEnd}
style={{
position: 'absolute',
left: position.x,
top: position.y,
width: '100px',
height: '100px',
backgroundColor: 'green',
touchAction: 'none'
}}
>
Drag me
</div>
);
};
Warning: Always set touchAction: 'none' CSS property when handling touch events to
prevent default browser behaviors. Use e.preventDefault() carefully as it can interfere with
scrolling.
Event Handling Best Practices
Function references : Use direct references when possible for better
performance
useCallback : Memoize handlers passed to memo'ed children or large lists
Event delegation : React handles automatically - don't manually manage
listeners
preventDefault/stopPropagation : Use judiciously to control event flow
Touch events : Support both mouse and touch for best mobile/desktop
experience
Debounce/throttle : Control frequency for expensive operations (search,
scroll)
8. Lists, Keys, and Dynamic Rendering
8.1 Array Rendering and map() Patterns
Pattern
Syntax
Description
Use Case
Basic map()
array.map(item => JSX)
Transform array to JSX elements
Simple list rendering
Index parameter
array.map((item, i) => JSX)
Access index in callback
Numbered lists, positioning
Array parameter
array.map((item, i, arr) => JSX)
Access full array
Context-aware rendering
Inline return
{items.map(i => <div />)}
Direct JSX return
Concise single elements
Block return
{items.map(i => { return <div /> })}
Explicit return statement
Multiple operations before return
Fragment wrapping
map(i => <>...</>)
Multiple elements without wrapper
Flat DOM structure
Destructuring
map(({id, name}) => JSX)
Extract properties directly
Cleaner variable access
Example: Basic list rendering patterns
// Simple list rendering
const UserList = ({ users }) => (
< ul >
{users. map ( user => (
< li key = {user.id}>{user.name}</ li >
))}
</ ul >
);
// With index for numbering
const NumberedList = ({ items }) => (
< ol >
{items. map (( item , index ) => (
< li key = {item.id}>
#{index + 1 }: {item.text}
</ li >
))}
</ ol >
);
// Destructuring in map
const ProductList = ({ products }) => (
< div >
{products. map (({ id , name , price , stock }) => (
< div key = {id} className = "product" >
< h3 >{name}</ h3 >
< p >${price. toFixed ( 2 )}</ p >
{stock < 10 && < span >Low stock!</ span >}
</ div >
))}
</ div >
);
// Multiple elements with fragment
const Timeline = ({ events }) => (
< div >
{events. map ( event => (
< React.Fragment key = {event.id}>
< h3 >{event.title}</ h3 >
< p >{event.description}</ p >
< time >{event.date}</ time >
< hr />
</ React.Fragment >
))}
</ div >
);
// Complex logic with block
const PostList = ({ posts }) => (
< div >
{posts. map (( post ) => {
const isNew = Date. now () - post.timestamp < 86400000 ;
const authorName = post.author.name || 'Anonymous' ;
return (
< article key = {post.id} className = {isNew ? 'new' : '' }>
< h2 >{post.title}</ h2 >
< p >By {authorName}</ p >
< p >{post.content}</ p >
</ article >
);
})}
</ div >
);
// Nested arrays
const Categories = ({ categories }) => (
< div >
{categories. map ( category => (
< div key = {category.id}>
< h2 >{category.name}</ h2 >
< ul >
{category.items. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
</ div >
))}
</ div >
);
Note: Always return JSX from map() - common mistake is forgetting the return statement in block
syntax. Use {} for expressions, not statements.
Key Type
Example
Stability
When to Use
Unique ID BEST
key={item.id}
Stable across renders
Items from database/API
Compound key
key={\`\${cat}-\${id}\`}
Stable if parts stable
Nested lists, multiple sources
Content hash
key={hash(item)}
Stable for same content
Static data, computed keys
Index AVOID
key={index}
Unstable on reorder
ONLY static immutable lists
Random/UUID NEVER
key={Math.random()}
Never stable
Never - causes re-mount
Key Rule
Description
Impact if Violated
Unique among siblings
Keys must be unique within array
Incorrect element updates, bugs
Stable across renders
Same item = same key
Unnecessary re-mounts, state loss
Not globally unique
Only unique in local array
No issue - scoped to parent
No array index for dynamic
Avoid index if items move/delete
Wrong items update, state mismatch
Don't use objects
Keys must be string/number
Warning, uses toString()
Example: Key prop patterns and anti-patterns
// ✅ GOOD: Stable unique IDs from data
const GoodList = ({ items }) => (
< ul >
{items. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
);
// ✅ GOOD: Compound keys for nested structures
const NestedGood = ({ categories }) => (
< div >
{categories. map ( cat => (
< div key = {cat.id}>
< h2 >{cat.name}</ h2 >
{cat.products. map ( prod => (
< div key = {\ ` \$ {cat.id}- \$ {prod.id} \` }>
{prod.name}
</div>
))}
</div>
))}
</div>
);
// ⚠️ ACCEPTABLE: Index only for static lists
const StaticList = () => {
const staticItems = ['Home', 'About', 'Contact']; // Never changes
return (
<ul>
{staticItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
};
// ❌ BAD: Index with dynamic operations
const BadDynamic = ({ items, onDelete }) => (
<ul>
{items.map((item, index) => (
<li key={index}> {/* BUG: Keys shift on delete */}
<input defaultValue={item.name} />
<button onClick={() => onDelete(index)}>Delete</button>
</li>
))}
</ul>
);
// ❌ BAD: Random/changing keys
const BadRandom = ({ items }) => (
<ul>
{items.map(item => (
<li key={Math.random()}> {/* Always re-mounts */}
{item.name}
</li>
))}
</ul>
);
// ❌ BAD: Non-unique keys
const BadDuplicate = ({ items }) => (
<ul>
{items.map(item => (
<li key={item.type}> {/* Multiple items same type */}
{item.name}
</li>
))}
</ul>
);
// ✅ GOOD: Generate stable ID if missing
const WithGeneratedID = ({ items }) => {
const itemsWithIds = useMemo(() =>
items.map((item, index) => ({
...item,
_key: item.id || \` generated- \$ {index}- \$ {item.name} \`
})),
[items]
);
return (
<ul>
{itemsWithIds.map(item => (
<li key={item._key}>{item.name}</li>
))}
</ul>
);
};
Warning: Using array index as key with reorderable/deletable lists causes bugs:
component state (like input values) gets assigned to wrong items after operations.
8.3 Dynamic List Updates and State Management
Operation
Pattern
Immutability
Add to end
[...items, newItem]
Creates new array
Add to start
[newItem, ...items]
Creates new array
Insert at index
[...items.slice(0, i), item, ...items.slice(i)]
New array with insertion
Remove by index
items.filter((_, i) => i !== index)
New array without item
Remove by ID
items.filter(i => i.id !== id)
New array without item
Update by index
items.map((i, idx) => idx === index ? {...i, prop} : i)
New array with updated item
Update by ID
items.map(i => i.id === id ? {...i, prop} : i)
New array with updated item
Replace all
setItems(newItems)
Complete replacement
Sort
[...items].sort(compareFn)
Copy then sort
Reverse
[...items].reverse()
Copy then reverse
Example: Dynamic list operations with immutable state
const TodoList = () => {
const [ todos , setTodos ] = useState ([]);
const [ input , setInput ] = useState ( '' );
// Add new todo
const addTodo = () => {
if (input. trim ()) {
setTodos ([ ... todos, {
id: Date. now (),
text: input,
completed: false
}]);
setInput ( '' );
}
};
// Delete todo by ID
const deleteTodo = ( id ) => {
setTodos (todos. filter ( todo => todo.id !== id));
};
// Toggle completed status
const toggleTodo = ( id ) => {
setTodos (todos. map ( todo =>
todo.id === id
? { ... todo, completed: ! todo.completed }
: todo
));
};
// Update todo text
const updateTodo = ( id , newText ) => {
setTodos (todos. map ( todo =>
todo.id === id
? { ... todo, text: newText }
: todo
));
};
// Move todo up
const moveUp = ( index ) => {
if (index > 0 ) {
const newTodos = [ ... todos];
[newTodos[index - 1 ], newTodos[index]] =
[newTodos[index], newTodos[index - 1 ]];
setTodos (newTodos);
}
};
// Clear completed
const clearCompleted = () => {
setTodos (todos. filter ( todo => ! todo.completed));
};
return (
< div >
< input
value = {input}
onChange = { e => setInput (e.target.value)}
onKeyPress = { e => e.key === 'Enter' && addTodo ()}
/>
< button onClick = {addTodo}>Add</ button >
< ul >
{todos. map (( todo , index ) => (
< li key = {todo.id}>
< input
type = "checkbox"
checked = {todo.completed}
onChange = {() => toggleTodo (todo.id)}
/>
< span style = {{
textDecoration: todo.completed ? 'line-through' : 'none'
}}>
{todo.text}
</ span >
< button onClick = {() => moveUp (index)}>↑</ button >
< button onClick = {() => deleteTodo (todo.id)}>Delete</ button >
</ li >
))}
</ ul >
< button onClick = {clearCompleted}>Clear Completed</ button >
</ div >
);
};
// Advanced: Batch updates with useReducer
const listReducer = ( state , action ) => {
switch (action.type) {
case 'ADD' :
return [ ... state, action.item];
case 'DELETE' :
return state. filter ( item => item.id !== action.id);
case 'UPDATE' :
return state. map ( item =>
item.id === action.id
? { ... item, ... action.updates }
: item
);
case 'REORDER' :
const result = [ ... state];
const [ removed ] = result. splice (action.from, 1 );
result. splice (action.to, 0 , removed);
return result;
case 'BULK_UPDATE' :
return state. map ( item =>
action.ids. includes (item.id)
? { ... item, ... action.updates }
: item
);
default :
return state;
}
};
const AdvancedList = () => {
const [ items , dispatch ] = useReducer (listReducer, []);
return (
< div >
< button onClick = {() => dispatch ({
type: 'ADD' ,
item: { id: Date. now (), text: 'New item' }
})}>
Add Item
</ button >
{items. map ( item => (
< div key = {item.id}>
{item.text}
< button onClick = {() => dispatch ({
type: 'DELETE' ,
id: item.id
})}>
Delete
</ button >
</ div >
))}
</ div >
);
};
Note: Never mutate state arrays directly (items.push(),
items[0] = x).
Always create new arrays to trigger React re-renders properly.
8.4 Conditional Rendering and Null Patterns
Pattern
Syntax
Renders
Use Case
Logical AND
{condition && <Component />}
Component or nothing
Simple show/hide
Ternary
{condition ? <A /> : <B />}
Either A or B
Two alternatives
Null coalescing
{value ?? 'default'}
Value or default
Null/undefined fallback
Optional chaining
{obj?.prop?.value}
Value or undefined
Safe nested access
Array filter
{arr.filter(cond).map(...)}
Filtered items
Conditional lists
Early return
if (!data) return null;
Nothing or rest
Loading/error states
Empty fragment
<>{condition && ...}</>
Conditional children
Multiple conditional elements
Nullish rendering
{value || 'N/A'}
Value or fallback
Display defaults
Example: Conditional rendering patterns
// Logical AND for simple conditions
const Greeting = ({ isLoggedIn , username }) => (
< div >
< h1 >Welcome</ h1 >
{isLoggedIn && < p >Hello, {username}!</ p >}
</ div >
);
// Ternary for either/or
const Status = ({ isOnline }) => (
< div >
{isOnline ? (
< span className = "online" >Online</ span >
) : (
< span className = "offline" >Offline</ span >
)}
</ div >
);
// Early returns for complex conditions
const UserProfile = ({ user , isLoading , error }) => {
if (isLoading) return < div >Loading...</ div >;
if (error) return < div >Error: {error.message}</ div >;
if ( ! user) return < div >No user found</ div >;
return (
< div >
< h1 >{user.name}</ h1 >
< p >{user.email}</ p >
</ div >
);
};
// Nested conditionals with optional chaining
const Address = ({ user }) => (
< div >
< p >{user?.address?.street ?? 'No address provided' }</ p >
< p >{user?.address?.city}, {user?.address?.country}</ p >
</ div >
);
// Multiple conditions in list
const ItemList = ({ items , showOnlyActive , sortByName }) => {
const filteredItems = items
. filter ( item => ! showOnlyActive || item.active)
. sort (( a , b ) => sortByName ? a.name. localeCompare (b.name) : 0 );
return (
< ul >
{filteredItems. length === 0 ? (
< li >No items to display</ li >
) : (
filteredItems. map ( item => (
< li key = {item.id}>
{item.name}
{item.isNew && < span className = "badge" >NEW</ span >}
</ li >
))
)}
</ ul >
);
};
// Complex multi-condition rendering
const Dashboard = ({ user , permissions , settings }) => (
< div >
{ /* Admin section */ }
{user.role === 'admin' && (
< section >
< h2 >Admin Panel</ h2 >
{permissions.canEdit && < button >Edit</ button >}
{permissions.canDelete && < button >Delete</ button >}
</ section >
)}
{ /* Premium features */ }
{user.isPremium ? (
< PremiumFeatures />
) : (
< div >
< p >Upgrade to Premium</ p >
< button >Upgrade Now</ button >
</ div >
)}
{ /* Settings based on flags */ }
{settings.showNotifications && < NotificationPanel />}
{settings.enableDarkMode && < ThemeToggle />}
</ div >
);
// Avoiding falsy value bugs
const Counter = ({ count }) => (
< div >
{ /* ❌ BAD: renders "0" when count is 0 */ }
{count && < p >Count: {count}</ p >}
{ /* ✅ GOOD: explicit check */ }
{count > 0 && < p >Count: {count}</ p >}
{ /* ✅ GOOD: ternary for all cases */ }
{count ? < p >Count: {count}</ p > : < p >No items</ p >}
</ div >
);
Warning: Watch out for falsy values in logical AND:
0 && <Component />
renders "0", not nothing. Use explicit boolean checks or ternary operators.
8.5 List Filtering and Search Implementation
Technique
Method
Performance
Use Case
Simple filter
items.filter(predicate)
O(n)
Small lists, simple conditions
Case-insensitive search
toLowerCase().includes()
O(n×m)
Text search
Multiple criteria
filter(i => cond1 && cond2)
O(n)
Advanced filtering
Debounced search
Delay filter execution
Reduced calls
Live search, API calls
Memoized filter
useMemo(() => filter)
Cached result
Expensive filters, large lists
Index search
Pre-built lookup maps
O(1) lookup
Very large datasets
Example: Search and filter implementations
// Basic search filter
const SearchList = () => {
const [ items ] = useState ([
{ id: 1 , name: 'Apple' , category: 'Fruit' },
{ id: 2 , name: 'Banana' , category: 'Fruit' },
{ id: 3 , name: 'Carrot' , category: 'Vegetable' }
]);
const [ search , setSearch ] = useState ( '' );
const filteredItems = items. filter ( item =>
item.name. toLowerCase (). includes (search. toLowerCase ())
);
return (
< div >
< input
value = {search}
onChange = { e => setSearch (e.target.value)}
placeholder = "Search..."
/>
< ul >
{filteredItems. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
</ div >
);
};
// Multi-criteria filter with memoization
const AdvancedFilter = () => {
const [ products , setProducts ] = useState ([]);
const [ filters , setFilters ] = useState ({
search: '' ,
category: 'all' ,
minPrice: 0 ,
maxPrice: 1000 ,
inStock: false
});
const filteredProducts = useMemo (() => {
return products. filter ( product => {
// Search filter
if (filters.search &&
! product.name. toLowerCase (). includes (filters.search. toLowerCase ())) {
return false ;
}
// Category filter
if (filters.category !== 'all' && product.category !== filters.category) {
return false ;
}
// Price range
if (product.price < filters.minPrice || product.price > filters.maxPrice) {
return false ;
}
// Stock filter
if (filters.inStock && product.stock === 0 ) {
return false ;
}
return true ;
});
}, [products, filters]);
return (
< div >
< input
value = {filters.search}
onChange = { e => setFilters ({ ... filters, search: e.target.value})}
placeholder = "Search products..."
/>
< select
value = {filters.category}
onChange = { e => setFilters ({ ... filters, category: e.target.value})}
>
< option value = "all" >All Categories</ option >
< option value = "electronics" >Electronics</ option >
< option value = "clothing" >Clothing</ option >
</ select >
< label >
< input
type = "checkbox"
checked = {filters.inStock}
onChange = { e => setFilters ({ ... filters, inStock: e.target.checked})}
/>
In Stock Only
</ label >
< p >Found {filteredProducts. length } products</ p >
< div >
{filteredProducts. map ( product => (
< div key = {product.id}>
< h3 >{product.name}</ h3 >
< p >${product.price}</ p >
< p >Stock: {product.stock}</ p >
</ div >
))}
</ div >
</ div >
);
};
// Debounced search for better performance
const useDebounce = ( value , delay ) => {
const [ debouncedValue , setDebouncedValue ] = useState (value);
useEffect (() => {
const handler = setTimeout (() => {
setDebouncedValue (value);
}, delay);
return () => clearTimeout (handler);
}, [value, delay]);
return debouncedValue;
};
const DebouncedSearch = () => {
const [ items ] = useState ([ /* large array */ ]);
const [ search , setSearch ] = useState ( '' );
const debouncedSearch = useDebounce (search, 300 );
const filteredItems = useMemo (() => {
if ( ! debouncedSearch) return items;
return items. filter ( item =>
item.name. toLowerCase (). includes (debouncedSearch. toLowerCase ())
);
}, [items, debouncedSearch]);
return (
< div >
< input
value = {search}
onChange = { e => setSearch (e.target.value)}
placeholder = "Search (debounced)..."
/>
< p >Searching for: {debouncedSearch}</ p >
< ul >
{filteredItems. map ( item => (
< li key = {item.id}>{item.name}</ li >
))}
</ ul >
</ div >
);
};
// Sorting combined with filtering
const SortableFilterable = () => {
const [ items , setItems ] = useState ([]);
const [ search , setSearch ] = useState ( '' );
const [ sortBy , setSortBy ] = useState ( 'name' );
const [ sortOrder , setSortOrder ] = useState ( 'asc' );
const processedItems = useMemo (() => {
// First filter
let result = items. filter ( item =>
item.name. toLowerCase (). includes (search. toLowerCase ())
);
// Then sort
result. sort (( a , b ) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
if ( typeof aVal === 'string' ) {
return sortOrder === 'asc'
? aVal. localeCompare (bVal)
: bVal. localeCompare (aVal);
}
return sortOrder === 'asc'
? aVal - bVal
: bVal - aVal;
});
return result;
}, [items, search, sortBy, sortOrder]);
return (
< div >
< input
value = {search}
onChange = { e => setSearch (e.target.value)}
/>
< select value = {sortBy} onChange = { e => setSortBy (e.target.value)}>
< option value = "name" >Name</ option >
< option value = "price" >Price</ option >
< option value = "date" >Date</ option >
</ select >
< button onClick = {() => setSortOrder (sortOrder === 'asc' ? 'desc' : 'asc' )}>
{sortOrder === 'asc' ? '↑' : '↓' }
</ button >
< ul >
{processedItems. map ( item => (
< li key = {item.id}>{item.name} - ${item.price}</ li >
))}
</ ul >
</ div >
);
};
Note: Use useMemo for expensive filter/sort operations on large lists.
Add debouncing for search inputs to reduce unnecessary re-renders.
8.6 Virtualized Lists and Large Dataset Handling
Technique
Description
Best For
Library
Windowing
Render only visible items
Long uniform lists
react-window
Virtual scrolling
Dynamic item height handling
Variable height items
react-virtualized
Infinite scroll
Load more on scroll bottom
Paginated data
react-infinite-scroll
Pagination
Page-based navigation
Discrete data chunks
Custom implementation
Lazy rendering
Render on intersection
Complex card layouts
IntersectionObserver
Cursor-based
Load by cursor position
Real-time feeds
API + state management
Example: Virtualized list with react-window
import { FixedSizeList } from 'react-window' ;
// Basic virtualized list
const VirtualList = ({ items }) => {
const Row = ({ index , style }) => (
< div style = {style}>
{items[index].name}
</ div >
);
return (
< FixedSizeList
height = { 600 }
itemCount = {items. length }
itemSize = { 50 }
width = "100%"
>
{Row}
</ FixedSizeList >
);
};
// Variable height list
import { VariableSizeList } from 'react-window' ;
const VariableHeightList = ({ items }) => {
const listRef = useRef ();
const getItemSize = ( index ) => {
// Calculate height based on content
return items[index].description. length > 100 ? 120 : 60 ;
};
const Row = ({ index , style }) => (
< div style = {style}>
< h4 >{items[index].title}</ h4 >
< p >{items[index].description}</ p >
</ div >
);
return (
< VariableSizeList
ref = {listRef}
height = { 600 }
itemCount = {items. length }
itemSize = {getItemSize}
width = "100%"
>
{Row}
</ VariableSizeList >
);
};
// Infinite scroll implementation
const InfiniteScrollList = () => {
const [ items , setItems ] = useState ([]);
const [ page , setPage ] = useState ( 1 );
const [ loading , setLoading ] = useState ( false );
const [ hasMore , setHasMore ] = useState ( true );
const observerRef = useRef ();
const lastItemRef = useCallback (( node ) => {
if (loading) return ;
if (observerRef.current) observerRef.current. disconnect ();
observerRef.current = new IntersectionObserver (( entries ) => {
if (entries[ 0 ].isIntersecting && hasMore) {
setPage ( prev => prev + 1 );
}
});
if (node) observerRef.current. observe (node);
}, [loading, hasMore]);
useEffect (() => {
const fetchMore = async () => {
setLoading ( true );
const response = await fetch (\ `/api/items?page= \$ {page} \` );
const newItems = await response.json();
setItems(prev => [...prev, ...newItems]);
setHasMore(newItems.length > 0);
setLoading(false);
};
fetchMore();
}, [page]);
return (
<div>
{items.map((item, index) => {
if (items.length === index + 1) {
return (
<div ref={lastItemRef} key={item.id}>
{item.name}
</div>
);
}
return <div key={item.id}>{item.name}</div>;
})}
{loading && <div>Loading...</div>}
</div>
);
};
// Pagination implementation
const PaginatedList = () => {
const [items, setItems] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const itemsPerPage = 20;
useEffect(() => {
const fetchPage = async () => {
const response = await fetch(
\` /api/items?page= \$ {currentPage}&limit= \$ {itemsPerPage} \`
);
const data = await response.json();
setItems(data.items);
setTotalPages(Math.ceil(data.total / itemsPerPage));
};
fetchPage();
}, [currentPage]);
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
);
};
// Lazy loading with intersection observer
const LazyList = ({ items }) => {
const [visibleItems, setVisibleItems] = useState(new Set());
const observeItem = useCallback((node, id) => {
if (!node) return;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
setVisibleItems(prev => new Set([...prev, id]));
observer.disconnect();
}
});
},
{ rootMargin: '100px' }
);
observer.observe(node);
}, []);
return (
<div>
{items.map(item => (
<div
key={item.id}
ref={node => observeItem(node, item.id)}
style={{ minHeight: '100px' }}
>
{visibleItems.has(item.id) ? (
<ExpensiveComponent item={item} />
) : (
<div>Loading...</div>
)}
</div>
))}
</div>
);
};
Performance Tips:
Use virtualization for lists > 100 items
Implement infinite scroll for continuous feeds
Prefer pagination for searchable/filterable data
Use IntersectionObserver for lazy loading images/components
Memoize row components with React.memo
9. Component Lifecycle and Effects
9.1 useEffect Hook Patterns and Dependencies
Pattern
Dependencies
Runs When
Use Case
No dependencies
useEffect(() => {})
Every render
Rarely needed, usually a mistake
Empty array
useEffect(() => {}, [])
Once on mount
Initial setup, subscriptions
With dependencies
useEffect(() => {}, [a, b])
When a or b changes
Sync with specific values
Single dependency
useEffect(() => {}, [id])
When id changes
Fetch data for specific ID
Object dependency
useEffect(() => {}, [obj])
When object reference changes
Can cause extra renders
Function dependency
useEffect(() => {}, [fn])
When function reference changes
Needs useCallback wrapper
Dependency Rule
Description
ESLint Rule
Include all used values
List all props, state, vars used in effect
exhaustive-deps
Primitive values
Numbers, strings, booleans - safe
No warning
Object/array deps
New reference = triggers effect
May need useMemo
Function deps
Wrap with useCallback
May need useCallback
Refs don't trigger
ref.current changes ignored
Safe to omit
setState stable
setState functions don't change
Safe to omit
Example: useEffect dependency patterns
// Run once on mount (componentDidMount equivalent)
useEffect (() => {
console. log ( 'Component mounted' );
document.title = 'My App' ;
}, []); // Empty dependency array
// Run on every render (usually wrong!)
useEffect (() => {
console. log ( 'Every render' ); // Performance issue!
}); // No dependency array - avoid this
// Run when specific values change
useEffect (() => {
console. log (\ `Count changed to \$ {count} \` );
}, [count]); // Re-runs when count changes
// Multiple dependencies
useEffect(() => {
console.log( \` User \$ {userId} on page \$ {page} \` );
fetchUserData(userId, page);
}, [userId, page]); // Re-runs when either changes
// Derived values in effect
const UserProfile = ({ userId }) => {
const [user, setUser] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchUser = async () => {
const data = await fetch( \` /api/users/ \$ {userId} \` ).then(r => r.json());
if (!cancelled) {
setUser(data);
}
};
fetchUser();
return () => {
cancelled = true;
};
}, [userId]); // Only userId dependency needed
return user ? <div>{user.name}</div> : <div>Loading...</div>;
};
// Object dependency issue
const BadObjectDep = ({ config }) => {
useEffect(() => {
// This runs every render if config is recreated!
console.log(config.apiUrl);
}, [config]); // ❌ Object reference changes each render
return null;
};
// Fix: Extract primitive values
const GoodObjectDep = ({ config }) => {
useEffect(() => {
console.log(config.apiUrl);
}, [config.apiUrl]); // ✅ Only re-run if apiUrl changes
return null;
};
// Function dependency issue
const Parent = () => {
const [count, setCount] = useState(0);
// ❌ New function every render
const handleClick = () => {
console.log(count);
};
return <Child onClick={handleClick} />;
};
const Child = ({ onClick }) => {
useEffect(() => {
// Runs every render because onClick changes
console.log('onClick changed');
}, [onClick]); // ❌ Function reference changes
return <button onClick={onClick}>Click</button>;
};
// Fix: Use useCallback
const ParentFixed = () => {
const [count, setCount] = useState(0);
// ✅ Stable function reference
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
return <ChildFixed onClick={handleClick} />;
};
// setState doesn't need to be in dependencies
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // ✅ Functional update
}, 1000);
return () => clearInterval(timer);
}, []); // setCount not needed in deps
return <div>{count}</div>;
};
Warning: React's ESLint plugin (eslint-plugin-react-hooks) will warn about
missing dependencies. Don't ignore these warnings - they prevent bugs. If you intentionally want to omit a
dependency, use // eslint-disable-next-line react-hooks/exhaustive-deps with a comment explaining
why.
9.2 Effect Cleanup and Memory Leak Prevention
Cleanup Scenario
Without Cleanup
With Cleanup
Memory Leak Risk
Timers
Continues running after unmount
clearTimeout/Interval
High
Event listeners
Accumulate on every mount
removeEventListener
High
Subscriptions
Connection stays open
unsubscribe()
High
Async operations
setState on unmounted component
Cancellation flag/AbortController
Medium
WebSocket
Connection remains open
socket.close()
High
Animation frames
Continues running
cancelAnimationFrame
Medium
Observers
Observers keep watching
observer.disconnect()
High
Example: Effect cleanup patterns
// Timer cleanup
const Timer = () => {
const [ seconds , setSeconds ] = useState ( 0 );
useEffect (() => {
const interval = setInterval (() => {
setSeconds ( s => s + 1 );
}, 1000 );
// Cleanup function
return () => {
clearInterval (interval);
console. log ( 'Timer cleaned up' );
};
}, []);
return < div >Seconds: {seconds}</ div >;
};
// Event listener cleanup
const WindowSize = () => {
const [ size , setSize ] = useState ({ width: 0 , height: 0 });
useEffect (() => {
const handleResize = () => {
setSize ({
width: window.innerWidth,
height: window.innerHeight
});
};
// Add listener
window. addEventListener ( 'resize' , handleResize);
handleResize (); // Initial call
// Cleanup: remove listener
return () => {
window. removeEventListener ( 'resize' , handleResize);
};
}, []);
return < div >{size.width} x {size.height}</ div >;
};
// Async operation cleanup (prevent setState on unmounted)
const UserData = ({ userId }) => {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
let cancelled = false ; // Cancellation flag
const fetchUser = async () => {
setLoading ( true );
try {
const response = await fetch (\ `/api/users/ \$ {userId} \` );
const data = await response.json();
// Only update state if not cancelled
if (!cancelled) {
setUser(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
fetchUser();
// Cleanup: set flag to prevent setState
return () => {
cancelled = true;
};
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
};
// AbortController for fetch cleanup (modern approach)
const DataFetcher = ({ endpoint }) => {
const [data, setData] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(endpoint, {
signal: controller.signal
});
const result = await response.json();
setData(result);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Fetch aborted');
} else {
console.error(error);
}
}
};
fetchData();
// Cleanup: abort fetch
return () => {
controller.abort();
};
}, [endpoint]);
return <div>{JSON.stringify(data)}</div>;
};
// WebSocket cleanup
const LiveChat = ({ roomId }) => {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket( \` wss://chat.example.com/ \$ {roomId} \` );
ws.onopen = () => {
console.log('Connected to chat');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup: close WebSocket
return () => {
ws.close();
console.log('Disconnected from chat');
};
}, [roomId]);
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
);
};
// IntersectionObserver cleanup
const LazyImage = ({ src, alt }) => {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect(); // Stop observing once visible
}
},
{ threshold: 0.1 }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
// Cleanup: disconnect observer
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={imgRef}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div>Loading...</div>
)}
</div>
);
};
// Animation frame cleanup
const AnimatedCounter = ({ target }) => {
const [count, setCount] = useState(0);
useEffect(() => {
let frame;
let startTime = null;
const animate = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = timestamp - startTime;
const nextCount = Math.min(
Math.floor((progress / 2000) * target),
target
);
setCount(nextCount);
if (nextCount < target) {
frame = requestAnimationFrame(animate);
}
};
frame = requestAnimationFrame(animate);
// Cleanup: cancel animation
return () => {
cancelAnimationFrame(frame);
};
}, [target]);
return <div>{count}</div>;
};
Warning: Always clean up side effects! Common memory leaks: forgetting to clear timers,
not removing event listeners, leaving WebSockets open, or calling setState on unmounted components.
9.3 Effect Timing and useLayoutEffect
Hook
Timing
Blocks Paint
Use Case
useEffect
After paint (async)
No
Most side effects, data fetching
useLayoutEffect
Before paint (sync)
Yes
DOM measurements, prevent flicker
Execution Order
Phase
Description
1. Render
Component function runs
JSX returned, state/props evaluated
2. React updates DOM
DOM mutations
Changes applied to DOM
3. useLayoutEffect
Before browser paint
Synchronous, blocks painting
4. Browser paints
Visual update
User sees changes
5. useEffect
After paint
Async, doesn't block paint
Example: useEffect vs useLayoutEffect timing
// useEffect - runs AFTER paint (preferred)
const DataComponent = ({ id }) => {
const [ data , setData ] = useState ( null );
useEffect (() => {
// Runs after component is painted
// Good for data fetching - doesn't block UI
fetchData (id). then (setData);
}, [id]);
return < div >{data?.name}</ div >;
};
// useLayoutEffect - runs BEFORE paint
const TooltipPosition = () => {
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
const tooltipRef = useRef ( null );
useLayoutEffect (() => {
// Measure DOM before browser paints
if (tooltipRef.current) {
const rect = tooltipRef.current. getBoundingClientRect ();
// Adjust position if off screen
let x = rect.left;
let y = rect.top;
if (x + rect.width > window.innerWidth) {
x = window.innerWidth - rect.width - 10 ;
}
if (y + rect.height > window.innerHeight) {
y = window.innerHeight - rect.height - 10 ;
}
setPosition ({ x, y });
}
}); // Runs on every render
return (
< div
ref = {tooltipRef}
style = {{ left: position.x, top: position.y }}
>
Tooltip
</ div >
);
};
// Prevent visual flicker with useLayoutEffect
const AnimatedHeight = ({ isOpen , children }) => {
const [ height , setHeight ] = useState ( 0 );
const contentRef = useRef ( null );
useLayoutEffect (() => {
if (isOpen && contentRef.current) {
// Measure and set height BEFORE paint
// Prevents flicker from 0 to auto height
const contentHeight = contentRef.current.scrollHeight;
setHeight (contentHeight);
} else {
setHeight ( 0 );
}
}, [isOpen]);
return (
< div
style = {{
height: height,
overflow: 'hidden' ,
transition: 'height 0.3s ease'
}}
>
< div ref = {contentRef}>{children}</ div >
</ div >
);
};
// DOM mutation before paint
const ScrollToTop = ({ trigger }) => {
useLayoutEffect (() => {
// Scroll happens before user sees the page
window. scrollTo ( 0 , 0 );
}, [trigger]);
return null ;
};
// Comparison: flicker with useEffect
const FlickerExample = () => {
const [ width , setWidth ] = useState ( 0 );
const ref = useRef ( null );
// ❌ WRONG: causes visible flicker
useEffect (() => {
// Runs AFTER paint - user sees wrong width first
if (ref.current) {
setWidth (ref.current.offsetWidth);
}
}, []);
return < div ref = {ref}>Width: {width}px</ div >;
};
// Fixed: no flicker with useLayoutEffect
const NoFlickerExample = () => {
const [ width , setWidth ] = useState ( 0 );
const ref = useRef ( null );
// ✅ CORRECT: measures before paint
useLayoutEffect (() => {
// Runs BEFORE paint - correct width from start
if (ref.current) {
setWidth (ref.current.offsetWidth);
}
}, []);
return < div ref = {ref}>Width: {width}px</ div >;
};
// Focus management with useLayoutEffect
const AutoFocus = ({ shouldFocus }) => {
const inputRef = useRef ( null );
useLayoutEffect (() => {
if (shouldFocus && inputRef.current) {
// Focus before paint - no visual jump
inputRef.current. focus ();
}
}, [shouldFocus]);
return < input ref = {inputRef} />;
};
Rule of thumb: Use useEffect for 99% of cases. Only use
useLayoutEffect
when you need to read layout (measurements, scroll position) or prevent visual flicker. useLayoutEffect can hurt
performance because it blocks painting.
9.4 Data Fetching with useEffect Patterns
Pattern
Best Practice
Issues to Handle
Basic fetch
async/await in effect
Loading state, errors
Cleanup
Cancellation flag or AbortController
setState on unmounted component
Dependencies
Include all params used in fetch
Stale data, race conditions
Race conditions
Ignore results from outdated requests
Wrong data displayed
Error handling
Try/catch and error state
Unhandled rejections
Loading state
Boolean flag while fetching
Poor UX without feedback
Example: Data fetching patterns
// Basic data fetching
const UserProfile = ({ userId }) => {
const [ user , setUser ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState ( null );
useEffect (() => {
const fetchUser = async () => {
try {
setLoading ( true );
setError ( null );
const response = await fetch (\ `/api/users/ \$ {userId} \` );
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
};
// With cleanup to prevent setState on unmounted
const DataFetchWithCleanup = ({ endpoint }) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
const response = await fetch(endpoint);
const result = await response.json();
if (!cancelled) {
setData(result);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [endpoint]);
return loading ? <div>Loading...</div> : <div>{JSON.stringify(data)}</div>;
};
// Race condition handling
const SearchResults = ({ query }) => {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
let cancelled = false;
setLoading(true);
const search = async () => {
try {
const response = await fetch( \` /api/search?q= \$ {query} \` );
const data = await response.json();
// Only update if this is still the latest query
if (!cancelled) {
setResults(data);
setLoading(false);
}
} catch (error) {
if (!cancelled) {
console.error(error);
setLoading(false);
}
}
};
search();
// Cleanup: ignore results if query changes
return () => {
cancelled = true;
};
}, [query]);
return (
<div>
{loading && <div>Searching...</div>}
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// AbortController pattern (modern)
const ModernFetch = ({ url }) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
signal: controller.signal
});
const result = await response.json();
setData(result);
} catch (err) {
// Don't set error if aborted
if (err.name !== 'AbortError') {
setError(err.message);
}
}
};
fetchData();
return () => {
controller.abort();
};
}, [url]);
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Debounced search
const DebouncedSearch = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
setLoading(true);
// Debounce: wait 500ms after user stops typing
const timer = setTimeout(async () => {
try {
const response = await fetch( \` /api/search?q= \$ {query} \` );
const data = await response.json();
setResults(data);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, 500);
// Cleanup: cancel previous timeout
return () => {
clearTimeout(timer);
};
}, [query]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
{loading && <div>Searching...</div>}
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// Multiple parallel requests
const Dashboard = () => {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
const [notifications, setNotifications] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAll = async () => {
try {
// Fetch all in parallel
const [userRes, postsRes, notifRes] = await Promise.all([
fetch('/api/user'),
fetch('/api/posts'),
fetch('/api/notifications')
]);
const [userData, postsData, notifData] = await Promise.all([
userRes.json(),
postsRes.json(),
notifRes.json()
]);
setUser(userData);
setPosts(postsData);
setNotifications(notifData);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
fetchAll();
}, []);
if (loading) return <div>Loading dashboard...</div>;
return (
<div>
<h1>Welcome {user?.name}</h1>
<div>Posts: {posts.length}</div>
<div>Notifications: {notifications.length}</div>
</div>
);
};
Warning: For production apps, consider using dedicated data fetching libraries like
SWR or React Query instead of manual useEffect. They handle caching, revalidation,
race conditions, and optimistic updates automatically.
9.5 Custom Hooks for Effect Logic
Custom Hook
Encapsulates
Returns
Reusability
useFetch
Data fetching logic
{data, loading, error}
High - any API endpoint
useDebounce
Debounced value updates
Debounced value
High - search, filters
useInterval
Timer with cleanup
void
Medium - polling
useEventListener
Event subscription
void
High - any event
useLocalStorage
localStorage sync
[value, setValue]
High - persistence
useWindowSize
Window dimensions
{width, height}
High - responsive
Example: Reusable custom hooks
// useFetch - reusable data fetching
const useFetch = ( url ) => {
const [ data , setData ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState ( null );
useEffect (() => {
if ( ! url) return ;
const controller = new AbortController ();
const fetchData = async () => {
try {
setLoading ( true );
const response = await fetch (url, {
signal: controller.signal
});
if ( ! response.ok) {
throw new Error (\ `HTTP error! status: \$ {response.status} \` );
}
const result = await response.json();
setData(result);
setError(null);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
};
// Usage
const UserProfile = ({ userId }) => {
const { data: user, loading, error } = useFetch( \` /api/users/ \$ {userId} \` );
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{user.name}</div>;
};
// useDebounce - delay value updates
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
// Usage
const SearchComponent = () => {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
const { data } = useFetch( \` /api/search?q= \$ {debouncedSearch} \` );
return (
<div>
<input value={search} onChange={e => setSearch(e.target.value)} />
<div>{JSON.stringify(data)}</div>
</div>
);
};
// useInterval - declarative setInterval
const useInterval = (callback, delay) => {
const savedCallback = useRef();
// Remember latest callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up interval
useEffect(() => {
if (delay === null) return;
const tick = () => {
savedCallback.current();
};
const id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
};
// Usage
const Clock = () => {
const [time, setTime] = useState(new Date());
useInterval(() => {
setTime(new Date());
}, 1000);
return <div>{time.toLocaleTimeString()}</div>;
};
// useEventListener - declarative addEventListener
const useEventListener = (eventName, handler, element = window) => {
const savedHandler = useRef();
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const isSupported = element && element.addEventListener;
if (!isSupported) return;
const eventListener = (event) => savedHandler.current(event);
element.addEventListener(eventName, eventListener);
return () => {
element.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
};
// Usage
const KeyLogger = () => {
const [key, setKey] = useState('');
useEventListener('keydown', (e) => {
setKey(e.key);
});
return <div>Last key: {key}</div>;
};
// useLocalStorage - sync state with localStorage
const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
const valueToStore = value instanceof Function
? value(storedValue)
: value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
};
// Usage
const ThemeToggle = () => {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
};
// useWindowSize - track window dimensions
const useWindowSize = () => {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
};
// Usage
const ResponsiveComponent = () => {
const { width } = useWindowSize();
return (
<div>
{width < 768 ? 'Mobile' : 'Desktop'} - {width}px
</div>
);
};
Note: Custom hooks let you extract and reuse effect logic across components. Name them with
use prefix and follow all hooks rules. They can use other hooks internally.
Optimization
Technique
Impact
When to Use
Proper dependencies
Only include what changes
Avoid unnecessary runs
Always
useMemo deps
Memoize object/array deps
Prevent extra effect runs
Object dependencies
useCallback deps
Memoize function deps
Stable function reference
Function dependencies
Split effects
One effect per concern
Run only what's needed
Multiple responsibilities
Functional updates
setState(prev => ...)
Remove state from deps
State-dependent updates
Debouncing
Delay effect execution
Reduce frequency
Frequent changes (input)
Throttling
Limit execution rate
Control frequency
Continuous events (scroll)
// ❌ BAD: Unnecessary effect runs
const BadDeps = ({ user }) => {
useEffect (() => {
console. log (user.name);
}, [user]); // Runs every time user object changes (even if name is same)
return null ;
};
// ✅ GOOD: Only depend on what you use
const GoodDeps = ({ user }) => {
useEffect (() => {
console. log (user.name);
}, [user.name]); // Only runs when name actually changes
return null ;
};
// ❌ BAD: Object dependency causes extra runs
const ParentBad = () => {
const [ count , setCount ] = useState ( 0 );
// New object every render!
const config = { apiUrl: 'https://api.example.com' };
return < ChildBad config = {config} />;
};
const ChildBad = ({ config }) => {
useEffect (() => {
fetch (config.apiUrl); // Runs every render!
}, [config]);
return null ;
};
// ✅ GOOD: Memoize object dependency
const ParentGood = () => {
const [ count , setCount ] = useState ( 0 );
// Same object reference across renders
const config = useMemo (
() => ({ apiUrl: 'https://api.example.com' }),
[]
);
return < ChildGood config = {config} />;
};
const ChildGood = ({ config }) => {
useEffect (() => {
fetch (config.apiUrl); // Only runs once
}, [config]);
return null ;
};
// ❌ BAD: Multiple concerns in one effect
const BadMultiEffect = ({ userId , theme }) => {
useEffect (() => {
// Fetch user data
fetchUser (userId). then (setUser);
// Apply theme
document.body.className = theme;
// Both run on every userId OR theme change!
}, [userId, theme]);
return null ;
};
// ✅ GOOD: Split into separate effects
const GoodMultiEffect = ({ userId , theme }) => {
// Effect 1: User data (only re-runs when userId changes)
useEffect (() => {
fetchUser (userId). then (setUser);
}, [userId]);
// Effect 2: Theme (only re-runs when theme changes)
useEffect (() => {
document.body.className = theme;
}, [theme]);
return null ;
};
// ❌ BAD: State in dependencies
const BadStateDep = () => {
const [ count , setCount ] = useState ( 0 );
useEffect (() => {
const timer = setInterval (() => {
setCount (count + 1 ); // Uses stale count
}, 1000 );
return () => clearInterval (timer);
}, [count]); // Re-creates interval on every count change!
return < div >{count}</ div >;
};
// ✅ GOOD: Functional update removes dependency
const GoodStateDep = () => {
const [ count , setCount ] = useState ( 0 );
useEffect (() => {
const timer = setInterval (() => {
setCount ( c => c + 1 ); // Functional update
}, 1000 );
return () => clearInterval (timer);
}, []); // No dependencies - interval created once
return < div >{count}</ div >;
};
// Debouncing to reduce effect frequency
const SearchWithDebounce = () => {
const [ searchTerm , setSearchTerm ] = useState ( '' );
const [ results , setResults ] = useState ([]);
useEffect (() => {
// Wait for user to stop typing
const timer = setTimeout (() => {
if (searchTerm) {
fetch (\ `/api/search?q= \$ {searchTerm} \` )
.then(r => r.json())
.then(setResults);
}
}, 500); // Debounce 500ms
return () => clearTimeout(timer);
}, [searchTerm]);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// Throttling for continuous events
const ScrollTracker = () => {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
let ticking = false;
const handleScroll = () => {
if (!ticking) {
window.requestAnimationFrame(() => {
setScrollY(window.scrollY);
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>Scroll position: {scrollY}px</div>;
};
// Conditional effect execution
const ConditionalEffect = ({ shouldFetch, userId }) => {
useEffect(() => {
// Early return to skip effect
if (!shouldFetch) return;
fetchUser(userId).then(setUser);
}, [shouldFetch, userId]);
return null;
};
10.1 React.memo and Component Memoization
Concept
Description
When to Use
Performance Impact
React.memo
HOC that memoizes component result
Expensive renders, pure components
Skips re-render if props unchanged
Shallow comparison
Default props comparison (===)
Primitive props
Fast, works for most cases
Custom comparison
Custom arePropsEqual function
Complex props, deep equality
Slower, use judiciously
When NOT to use
Simple components, always different props
Overhead > benefit
Can hurt performance
Props Type
Memo Behavior
Solution
Primitives
Compared by value
Works perfectly
Objects
Compared by reference
useMemo to stabilize
Arrays
Compared by reference
useMemo to stabilize
Functions
Compared by reference
useCallback to stabilize
Children prop
New reference each render
Extract to separate component
Example: React.memo usage patterns
// Basic React.memo usage
const ExpensiveComponent = React. memo (({ data }) => {
console. log ( 'Rendering ExpensiveComponent' );
// Expensive computation or large render tree
const processedData = data. map ( item => {
// Complex processing
return item.value * 2 ;
});
return (
< div >
{processedData. map ( val => (
< div key = {val}>{val}</ div >
))}
</ div >
);
});
// Parent component
const Parent = () => {
const [ count , setCount ] = useState ( 0 );
const [ data ] = useState ([ 1 , 2 , 3 , 4 , 5 ]);
return (
< div >
< button onClick = {() => setCount (count + 1 )}>
Count: {count}
</ button >
{ /* ExpensiveComponent won't re-render when count changes */ }
< ExpensiveComponent data = {data} />
</ div >
);
};
// ❌ BAD: memo doesn't help with new object references
const BadMemo = React. memo (({ config }) => {
return < div >{config.name}</ div >;
});
const ParentBad = () => {
const [ count , setCount ] = useState ( 0 );
return (
< div >
< button onClick = {() => setCount (count + 1 )}>{count}</ button >
{ /* Re-renders every time - new object! */ }
< BadMemo config = {{ name: 'Test' }} />
</ div >
);
};
// ✅ GOOD: stabilize object with useMemo
const GoodMemo = React. memo (({ config }) => {
return < div >{config.name}</ div >;
});
const ParentGood = () => {
const [ count , setCount ] = useState ( 0 );
const config = useMemo (() => ({ name: 'Test' }), []);
return (
< div >
< button onClick = {() => setCount (count + 1 )}>{count}</ button >
{ /* Only renders once! */ }
< GoodMemo config = {config} />
</ div >
);
};
// Custom comparison function
const UserCard = React. memo (
({ user , onEdit }) => {
return (
< div >
< h3 >{user.name}</ h3 >
< p >{user.email}</ p >
< button onClick = {() => onEdit (user.id)}>Edit</ button >
</ div >
);
},
( prevProps , nextProps ) => {
// Custom comparison: only re-render if user.id or user.name changed
return (
prevProps.user.id === nextProps.user.id &&
prevProps.user.name === nextProps.user.name
);
// Return true to skip render, false to render
}
);
// When NOT to use React.memo
const SimpleComponent = ({ text }) => {
return < div >{text}</ div >;
};
// No need for memo - component is very simple
const AlwaysChanging = ({ timestamp }) => {
return < div >{timestamp}</ div >;
};
// No need for memo - timestamp always changes
// Memoizing list items
const TodoItem = React. memo (({ todo , onToggle , onDelete }) => {
console. log ( 'Rendering todo:' , todo.id);
return (
< li >
< input
type = "checkbox"
checked = {todo.completed}
onChange = {() => onToggle (todo.id)}
/>
{todo.text}
< button onClick = {() => onDelete (todo.id)}>Delete</ button >
</ li >
);
});
const TodoList = () => {
const [ todos , setTodos ] = useState ([]);
// Stabilize callback functions
const handleToggle = useCallback (( id ) => {
setTodos ( prev => prev. map ( todo =>
todo.id === id ? { ... todo, completed: ! todo.completed } : todo
));
}, []);
const handleDelete = useCallback (( id ) => {
setTodos ( prev => prev. filter ( todo => todo.id !== id));
}, []);
return (
< ul >
{todos. map ( todo => (
< TodoItem
key = {todo.id}
todo = {todo}
onToggle = {handleToggle}
onDelete = {handleDelete}
/>
))}
</ ul >
);
};
Note: React.memo only checks props changes. It doesn't prevent re-renders from state/context
changes within the component. Use it for expensive components with stable props.
10.2 useMemo for Expensive Calculations
Use Case
Without useMemo
With useMemo
Benefit
Expensive calculation
Runs every render
Cached until deps change
Saves computation time
Array filtering/sorting
Creates new array every render
Reuses array if deps unchanged
Prevents unnecessary work
Object creation
New reference every render
Stable reference
Prevents child re-renders
Derived state
Computed on each render
Computed only when needed
Performance optimization
When to Use
When NOT to Use
Expensive calculations (loops, recursion)
Simple calculations (arithmetic)
Processing large arrays/objects
Small data sets (< 100 items)
Stabilizing references for child props
Values that always change
Complex transformations/filtering
Trivial operations
Example: useMemo patterns
// Expensive calculation
const ProductList = ({ products , filter }) => {
// Without useMemo: runs on EVERY render (bad!)
// const filtered = products.filter(p => p.category === filter);
// With useMemo: only runs when products or filter changes
const filteredProducts = useMemo (() => {
console. log ( 'Filtering products...' );
return products. filter ( p => p.category === filter);
}, [products, filter]);
return (
< ul >
{filteredProducts. map ( p => (
< li key = {p.id}>{p.name}</ li >
))}
</ ul >
);
};
// Complex derived state
const Dashboard = ({ data }) => {
const statistics = useMemo (() => {
console. log ( 'Computing statistics...' );
const total = data. reduce (( sum , item ) => sum + item.value, 0 );
const average = total / data. length ;
const max = Math. max ( ... data. map ( item => item.value));
const min = Math. min ( ... data. map ( item => item.value));
return { total, average, max, min };
}, [data]);
return (
< div >
< p >Total: {statistics.total}</ p >
< p >Average: {statistics.average}</ p >
< p >Max: {statistics.max}</ p >
< p >Min: {statistics.min}</ p >
</ div >
);
};
// Sorting large lists
const SortableTable = ({ data , sortBy , sortOrder }) => {
const sortedData = useMemo (() => {
console. log ( 'Sorting data...' );
return [ ... data]. sort (( a , b ) => {
const aVal = a[sortBy];
const bVal = b[sortBy];
if ( typeof aVal === 'string' ) {
return sortOrder === 'asc'
? aVal. localeCompare (bVal)
: bVal. localeCompare (aVal);
}
return sortOrder === 'asc' ? aVal - bVal : bVal - aVal;
});
}, [data, sortBy, sortOrder]);
return (
< table >
{sortedData. map ( row => (
< tr key = {row.id}>
< td >{row.name}</ td >
</ tr >
))}
</ table >
);
};
// Stabilizing object references
const Parent = () => {
const [ count , setCount ] = useState ( 0 );
// ❌ BAD: new object every render
// const config = { theme: 'dark', lang: 'en' };
// ✅ GOOD: stable reference
const config = useMemo (
() => ({ theme: 'dark' , lang: 'en' }),
[]
);
return (
< div >
< button onClick = {() => setCount (count + 1 )}>{count}</ button >
< MemoizedChild config = {config} />
</ div >
);
};
// When NOT to use useMemo
const Bad = ({ a , b }) => {
// ❌ Overkill - simple calculation
const sum = useMemo (() => a + b, [a, b]);
// ✅ Just do it directly
const betterSum = a + b;
return < div >{sum}</ div >;
};
// Search and filter with multiple criteria
const AdvancedSearch = ({ items , search , filters }) => {
const filteredItems = useMemo (() => {
console. log ( 'Filtering items...' );
return items. filter ( item => {
// Text search
if (search && ! item.name. toLowerCase (). includes (search. toLowerCase ())) {
return false ;
}
// Category filter
if (filters.category && item.category !== filters.category) {
return false ;
}
// Price range
if (item.price < filters.minPrice || item.price > filters.maxPrice) {
return false ;
}
return true ;
});
}, [items, search, filters]);
const sortedItems = useMemo (() => {
console. log ( 'Sorting items...' );
return [ ... filteredItems]. sort (( a , b ) => a.name. localeCompare (b.name));
}, [filteredItems]);
return (
< ul >
{sortedItems. map ( item => (
< li key = {item.id}>{item.name} - ${item.price}</ li >
))}
</ ul >
);
};
// Recursive computation
const TreeView = ({ node }) => {
const descendantCount = useMemo (() => {
const countDescendants = ( n ) => {
if ( ! n.children) return 0 ;
return n.children. length +
n.children. reduce (( sum , child ) => sum + countDescendants (child), 0 );
};
return countDescendants (node);
}, [node]);
return < div >Descendants: {descendantCount}</ div >;
};
Warning: Don't overuse useMemo! It has overhead (memory + comparison). Profile first, optimize
later. Most calculations are fast enough without memoization.
10.3 useCallback for Function Memoization
Scenario
Without useCallback
With useCallback
Impact
Function prop to memo component
New function = child re-renders
Stable function = no re-render
Prevents unnecessary renders
Function in useEffect deps
Effect runs every render
Effect runs only when needed
Reduces side effects
Event handlers
New function every render
Stable reference
Slight memory improvement
When to Use useCallback
When NOT to Use
Function passed to memoized child
Simple event handlers not passed down
Function in useEffect dependencies
Function only used once
Function passed to many children
Non-memoized children
Optimization after profiling
Premature optimization
Example: useCallback patterns
// Basic useCallback usage
const Parent = () => {
const [ count , setCount ] = useState ( 0 );
const [ text , setText ] = useState ( '' );
// ❌ BAD: new function every render
// const handleClick = () => {
// console.log(count);
// };
// ✅ GOOD: stable function reference
const handleClick = useCallback (() => {
console. log (count);
}, [count]); // Re-created only when count changes
return (
< div >
< input value = {text} onChange = { e => setText (e.target.value)} />
< button onClick = {() => setCount (count + 1 )}>{count}</ button >
{ /* Child won't re-render when text changes */ }
< MemoizedChild onClick = {handleClick} />
</ div >
);
};
const MemoizedChild = React. memo (({ onClick }) => {
console. log ( 'Child rendered' );
return < button onClick = {onClick}>Click me</ button >;
});
// Function in useEffect dependencies
const DataFetcher = ({ userId }) => {
const [ data , setData ] = useState ( null );
// Stable fetchData function
const fetchData = useCallback ( async () => {
const response = await fetch (\ `/api/users/ \$ {userId} \` );
const result = await response.json();
setData(result);
}, [userId]);
useEffect(() => {
fetchData();
}, [fetchData]); // Only runs when fetchData changes (i.e., when userId changes)
return <div>{data?.name}</div>;
};
// List operations with callbacks
const TodoList = () => {
const [todos, setTodos] = useState([]);
const handleToggle = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []); // Empty deps - uses functional update
const handleDelete = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
const handleEdit = useCallback((id, newText) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
}, []);
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</ul>
);
};
// When NOT to use useCallback
const SimpleComponent = () => {
const [count, setCount] = useState(0);
// ❌ Unnecessary - not passed to memoized child
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
// ✅ Just use inline
return (
<button onClick={() => setCount(c => c + 1)}>
{count}
</button>
);
};
// Combining useCallback with other hooks
const SearchComponent = () => {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const search = useCallback(async (searchQuery) => {
if (!searchQuery) {
setResults([]);
return;
}
const response = await fetch( \` /api/search?q= \$ {searchQuery} \` );
const data = await response.json();
setResults(data);
}, []);
// Debounced search
useEffect(() => {
const timer = setTimeout(() => {
search(query);
}, 300);
return () => clearTimeout(timer);
}, [query, search]);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
/>
<ul>
{results.map(r => (
<li key={r.id}>{r.title}</li>
))}
</ul>
</div>
);
};
// useCallback with parameters from closure
const ItemList = ({ items, onUpdate }) => {
const [filter, setFilter] = useState('');
// ❌ BAD: depends on filter but not in deps
// const handleUpdate = useCallback((id, value) => {
// if (filter === 'active') {
// onUpdate(id, value);
// }
// }, []); // Missing filter!
// ✅ GOOD: includes all dependencies
const handleUpdate = useCallback((id, value) => {
if (filter === 'active') {
onUpdate(id, value);
}
}, [filter, onUpdate]);
return (
<ul>
{items.map(item => (
<Item key={item.id} item={item} onUpdate={handleUpdate} />
))}
</ul>
);
};
Note: useCallback returns memoized function, useMemo returns memoized value.
useCallback(fn, deps) is equivalent to useMemo(() => fn, deps).
10.4 Code Splitting with React.lazy and Suspense
Technique
Description
Bundle Impact
Use Case
React.lazy
Dynamic import for components
Separate chunk per lazy component
Route-based splitting
Suspense
Loading fallback for lazy components
No impact
Show loading state
Route-based
Split by routes/pages
One chunk per route
Most common pattern
Component-based
Split heavy components
Smaller chunks
Modals, tabs, charts
Library splitting
Separate vendor chunks
Better caching
Large dependencies
Example: Code splitting patterns
// Basic React.lazy usage
import { lazy, Suspense } from 'react' ;
const HeavyComponent = lazy (() => import ( './HeavyComponent' ));
const App = () => {
return (
< Suspense fallback = {< div >Loading...</ div >}>
< HeavyComponent />
</ Suspense >
);
};
// Route-based code splitting
import { BrowserRouter, Routes, Route } from 'react-router-dom' ;
const Home = lazy (() => import ( './pages/Home' ));
const About = lazy (() => import ( './pages/About' ));
const Dashboard = lazy (() => import ( './pages/Dashboard' ));
const Profile = lazy (() => import ( './pages/Profile' ));
const AppRouter = () => {
return (
< BrowserRouter >
< Suspense fallback = {< LoadingSpinner />}>
< Routes >
< Route path = "/" element = {< Home />} />
< Route path = "/about" element = {< About />} />
< Route path = "/dashboard" element = {< Dashboard />} />
< Route path = "/profile" element = {< Profile />} />
</ Routes >
</ Suspense >
</ BrowserRouter >
);
};
// Conditional lazy loading
const AdminPanel = lazy (() => import ( './AdminPanel' ));
const App = ({ user }) => {
return (
< div >
{user.isAdmin && (
< Suspense fallback = {< div >Loading admin panel...</ div >}>
< AdminPanel />
</ Suspense >
)}
</ div >
);
};
// Named exports with lazy
const LazyComponent = lazy (() =>
import ( './MyComponent' ). then ( module => ({
default: module .MyNamedComponent
}))
);
// Preloading lazy components
const HeavyChart = lazy (() => import ( './HeavyChart' ));
// Preload function
const preloadChart = () => {
import ( './HeavyChart' );
};
const Dashboard = () => {
const [ showChart , setShowChart ] = useState ( false );
return (
< div >
{ /* Preload on hover */ }
< button
onClick = {() => setShowChart ( true )}
onMouseEnter = {preloadChart}
>
Show Chart
</ button >
{showChart && (
< Suspense fallback = {< div >Loading chart...</ div >}>
< HeavyChart />
</ Suspense >
)}
</ div >
);
};
// Modal lazy loading
const Modal = lazy (() => import ( './Modal' ));
const App = () => {
const [ isOpen , setIsOpen ] = useState ( false );
return (
< div >
< button onClick = {() => setIsOpen ( true )}>Open Modal</ button >
{isOpen && (
< Suspense fallback = { null }>
< Modal onClose = {() => setIsOpen ( false )} />
</ Suspense >
)}
</ div >
);
};
// Tab-based splitting
const Tab1 = lazy (() => import ( './tabs/Tab1' ));
const Tab2 = lazy (() => import ( './tabs/Tab2' ));
const Tab3 = lazy (() => import ( './tabs/Tab3' ));
const TabbedInterface = () => {
const [ activeTab , setActiveTab ] = useState ( 'tab1' );
return (
< div >
< div >
< button onClick = {() => setActiveTab ( 'tab1' )}>Tab 1</ button >
< button onClick = {() => setActiveTab ( 'tab2' )}>Tab 2</ button >
< button onClick = {() => setActiveTab ( 'tab3' )}>Tab 3</ button >
</ div >
< Suspense fallback = {< div >Loading tab...</ div >}>
{activeTab === 'tab1' && < Tab1 />}
{activeTab === 'tab2' && < Tab2 />}
{activeTab === 'tab3' && < Tab3 />}
</ Suspense >
</ div >
);
};
// Error boundary with lazy loading
class ErrorBoundary extends React . Component {
state = { hasError: false };
static getDerivedStateFromError ( error ) {
return { hasError: true };
}
render () {
if ( this .state.hasError) {
return < div >Failed to load component</ div >;
}
return this .props.children;
}
}
const AppWithErrorHandling = () => {
return (
< ErrorBoundary >
< Suspense fallback = {< div >Loading...</ div >}>
< LazyComponent />
</ Suspense >
</ ErrorBoundary >
);
};
// Webpack magic comments for chunk naming
const Dashboard = lazy (() =>
import ( /* webpackChunkName: "dashboard" */ './Dashboard' )
);
const AdminPanel = lazy (() =>
import ( /* webpackChunkName: "admin" */ './AdminPanel' )
);
// Prefetching/preloading
const Settings = lazy (() =>
import (
/* webpackChunkName: "settings" */
/* webpackPrefetch: true */
'./Settings'
)
);
Note: Code splitting works best for route-based splits and heavy components (charts, editors,
admin panels). Don't split every component - bundle overhead can outweigh benefits for small components.
10.5 Bundle Analysis and Optimization Strategies
Tool
Purpose
Install/Usage
webpack-bundle-analyzer
Visualize bundle contents
npm i -D webpack-bundle-analyzer
source-map-explorer
Analyze source maps
npm i -D source-map-explorer
Bundle Buddy
Find duplicate dependencies
Upload webpack stats.json
Lighthouse
Performance audit
Chrome DevTools
Optimization
Technique
Impact
Tree shaking
Remove unused exports
Smaller bundle (10-30%)
Minification
Compress code
40-60% size reduction
Compression (gzip/brotli)
Server-side compression
60-80% transfer size reduction
Dynamic imports
Load code on demand
Faster initial load
Remove unused deps
Audit package.json
Variable savings
Lighter alternatives
Replace heavy libraries
Significant size reduction
Example: Bundle optimization techniques
// package.json - analyze bundle
{
"scripts" : {
"analyze" : "source-map-explorer 'build/static/js/*.js'" ,
"build:analyze" : "npm run build && npm run analyze"
}
}
// webpack.config.js - bundle analyzer
const BundleAnalyzerPlugin = require ( 'webpack-bundle-analyzer' ).BundleAnalyzerPlugin;
module . exports = {
plugins: [
new BundleAnalyzerPlugin ({
analyzerMode: 'static' ,
openAnalyzer: false ,
reportFilename: 'bundle-report.html'
})
]
};
// Tree shaking - import only what you need
// ❌ BAD: imports entire library
import _ from 'lodash' ;
const result = _. debounce (fn, 300 );
// ✅ GOOD: import specific function
import debounce from 'lodash/debounce' ;
const result = debounce (fn, 300 );
// Or use lodash-es for better tree shaking
import { debounce } from 'lodash-es' ;
// Replace heavy libraries with lighter alternatives
// ❌ Moment.js (large)
import moment from 'moment' ;
// ✅ date-fns (smaller, tree-shakeable)
import { format, parseISO } from 'date-fns' ;
// ✅ dayjs (smallest)
import dayjs from 'dayjs' ;
// Remove unused dependencies
// Run: npm ls
// Check: npx depcheck
// Dynamic imports for heavy dependencies
const loadChartLibrary = async () => {
const Chart = await import ( 'chart.js' );
return Chart.default;
};
// Optimize images
// Use next/image or similar
import Image from 'next/image' ;
< Image
src = "/photo.jpg"
width = { 500 }
height = { 300 }
loading = "lazy"
alt = "Photo"
/>
// Code splitting by route
const routes = [
{
path: '/' ,
component: lazy (() => import ( './pages/Home' ))
},
{
path: '/dashboard' ,
component: lazy (() => import ( './pages/Dashboard' ))
}
];
// Vendor chunk separation (webpack)
optimization : {
splitChunks : {
chunks : 'all' ,
cacheGroups : {
vendor : {
test : / [ \\ /] node_modules [ \\ /] / ,
name : 'vendors' ,
priority : 10
},
common : {
minChunks : 2 ,
priority : 5 ,
reuseExistingChunk : true
}
}
}
}
// Preload critical resources
< link rel = "preload" as = "script" href = "/main.js" />
< link rel = "prefetch" href = "/secondary.js" />
// Compression middleware (Express)
const compression = require ( 'compression' );
app. use ( compression ());
// Service worker for caching
if ( 'serviceWorker' in navigator) {
navigator.serviceWorker. register ( '/sw.js' );
}
// Remove console logs in production
// babel-plugin-transform-remove-console
{
"plugins" : [
[ "transform-remove-console" , { "exclude" : [ "error" , "warn" ] }]
]
}
Warning: Always analyze your bundle before optimizing. Focus on the biggest chunks first.
Remember: premature optimization is the root of all evil!
10.6 Re-render Optimization and Profiling
Tool
Purpose
How to Use
React DevTools Profiler
Record and analyze renders
Click "Profiler" tab, record session
Highlight Updates
Visualize re-renders
Settings → "Highlight updates"
Component Stack
See render cause
Click component in Profiler
Why Did You Render
Debug unnecessary renders
npm package with detailed logs
Common Issue
Symptom
Solution
Inline functions
Child re-renders unnecessarily
useCallback + React.memo
Inline objects
Props always different
useMemo for stable reference
Context changes
All consumers re-render
Split contexts, memoize value
State too high
Entire tree re-renders
Move state closer to usage
Large lists
Slow rendering
Virtualization, pagination
No keys/bad keys
Wrong items update
Stable unique keys
Example: Debugging and fixing re-renders
// Install why-did-you-render
import React from 'react' ;
if (process.env. NODE_ENV === 'development' ) {
const whyDidYouRender = require ( '@welldone-software/why-did-you-render' );
whyDidYouRender (React, {
trackAllPureComponents: true ,
});
}
// Use in component
const MyComponent = ({ value }) => {
return < div >{value}</ div >;
};
MyComponent.whyDidYouRender = true ;
// Finding render causes with useEffect
const DebugComponent = ({ prop1 , prop2 , prop3 }) => {
useEffect (() => {
console. log ( 'Component rendered' );
console. log ( 'prop1:' , prop1);
console. log ( 'prop2:' , prop2);
console. log ( 'prop3:' , prop3);
});
return < div >Debug</ div >;
};
// Custom hook to track prop changes
const useWhyDidYouUpdate = ( name , props ) => {
const previousProps = useRef ();
useEffect (() => {
if (previousProps.current) {
const allKeys = Object. keys ({ ... previousProps.current, ... props });
const changedProps = {};
allKeys. forEach ( key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object. keys (changedProps). length > 0 ) {
console. log ( '[why-did-you-update]' , name, changedProps);
}
}
previousProps.current = props;
});
};
// Usage
const MyComponent = ( props ) => {
useWhyDidYouUpdate ( 'MyComponent' , props);
return < div >{props.value}</ div >;
};
// Fix: Inline object props
// ❌ BAD
const Parent = () => {
return < Child style = {{ color: 'red' }} />; // New object every render
};
// ✅ GOOD
const Parent = () => {
const style = useMemo (() => ({ color: 'red' }), []);
return < Child style = {style} />;
};
// Fix: Context re-renders
// ❌ BAD: value changes every render
const ThemeProvider = ({ children }) => {
const [ theme , setTheme ] = useState ( 'light' );
return (
< ThemeContext.Provider value = {{ theme, setTheme }}>
{children}
</ ThemeContext.Provider >
);
};
// ✅ GOOD: stable value reference
const ThemeProvider = ({ children }) => {
const [ theme , setTheme ] = useState ( 'light' );
const value = useMemo (
() => ({ theme, setTheme }),
[theme]
);
return (
< ThemeContext.Provider value = {value}>
{children}
</ ThemeContext.Provider >
);
};
// Fix: State colocation
// ❌ BAD: state too high
const App = () => {
const [ formData , setFormData ] = useState ({});
return (
< div >
< Header /> { /* Re-renders on formData change */ }
< Sidebar /> { /* Re-renders on formData change */ }
< Form data = {formData} onChange = {setFormData} />
</ div >
);
};
// ✅ GOOD: state colocated
const App = () => {
return (
< div >
< Header />
< Sidebar />
< FormContainer /> { /* State inside here */ }
</ div >
);
};
const FormContainer = () => {
const [ formData , setFormData ] = useState ({});
return < Form data = {formData} onChange = {setFormData} />;
};
// Profiling with React.Profiler API
const App = () => {
const onRenderCallback = (
id , // component name
phase , // "mount" or "update"
actualDuration , // time spent rendering
baseDuration , // estimated time without memoization
startTime , // when started
commitTime , // when committed
interactions // Set of interactions
) => {
console. log (\ ` \$ {id} took \$ {actualDuration}ms to render \` );
};
return (
<Profiler id="App" onRender={onRenderCallback}>
<MyApp />
</Profiler>
);
};
11. Error Handling and Error Boundaries
11.1 Error Boundary Components and Implementation
Concept
Description
Catches
Does NOT Catch
Error Boundary
Class component that catches errors in child tree
Render errors, lifecycle errors
Event handlers, async, SSR
Placement
Wrap components that might error
All descendants
Errors in boundary itself
Granularity
Multiple boundaries for isolation
Specific sections only
Unrelated components
Error Type
Caught by Boundary?
Handling Approach
Render errors
✅ Yes
Error boundary
Lifecycle methods
✅ Yes
Error boundary
Event handlers
❌ No
try/catch in handler
Async code (setTimeout)
❌ No
try/catch in callback
Server-side rendering
❌ No
Server error handling
Error in boundary itself
❌ No
Parent error boundary
Example: Error boundary implementation
// Basic error boundary class component
class ErrorBoundary extends React . Component {
constructor ( props ) {
super (props);
this .state = { hasError: false , error: null };
}
static getDerivedStateFromError ( error ) {
// Update state so next render shows fallback UI
return { hasError: true , error };
}
componentDidCatch ( error , errorInfo ) {
// Log error to reporting service
console. error ( 'Error caught by boundary:' , error, errorInfo);
// logErrorToService(error, errorInfo);
}
render () {
if ( this .state.hasError) {
return (
< div >
< h1 >Something went wrong.</ h1 >
< details >
< summary >Error details</ summary >
< pre >{ this .state.error. toString ()}</ pre >
</ details >
</ div >
);
}
return this .props.children;
}
}
// Usage - wrap components
const App = () => (
< ErrorBoundary >
< Header />
< Main />
< Footer />
</ ErrorBoundary >
);
// Multiple boundaries for isolation
const Dashboard = () => (
< div >
< ErrorBoundary >
< Sidebar />
</ ErrorBoundary >
< ErrorBoundary >
< MainContent />
</ ErrorBoundary >
< ErrorBoundary >
< Widgets />
</ ErrorBoundary >
</ div >
);
// Enhanced error boundary with retry
class ErrorBoundaryWithRetry extends React . Component {
constructor ( props ) {
super (props);
this .state = {
hasError: false ,
error: null ,
errorInfo: null
};
}
static getDerivedStateFromError ( error ) {
return { hasError: true , error };
}
componentDidCatch ( error , errorInfo ) {
this . setState ({ errorInfo });
console. error ( 'Error:' , error);
console. error ( 'Component stack:' , errorInfo.componentStack);
}
handleReset = () => {
this . setState ({
hasError: false ,
error: null ,
errorInfo: null
});
};
render () {
if ( this .state.hasError) {
return (
< div className = "error-container" >
< h2 >Oops! Something went wrong</ h2 >
< p >{ this .state.error?.message}</ p >
< button onClick = { this .handleReset}>
Try Again
</ button >
{process.env. NODE_ENV === 'development' && (
< details >
< summary >Stack trace</ summary >
< pre >{ this .state.errorInfo?.componentStack}</ pre >
</ details >
)}
</ div >
);
}
return this .props.children;
}
}
// Custom fallback component
const ErrorFallback = ({ error , resetError }) => (
< div role = "alert" >
< h2 >Something went wrong</ h2 >
< pre style = {{ color: 'red' }}>{error.message}</ pre >
< button onClick = {resetError}>Try again</ button >
</ div >
);
class ErrorBoundaryWithFallback extends React . Component {
constructor ( props ) {
super (props);
this .state = { hasError: false , error: null };
}
static getDerivedStateFromError ( error ) {
return { hasError: true , error };
}
componentDidCatch ( error , errorInfo ) {
this .props. onError ?.(error, errorInfo);
}
resetError = () => {
this . setState ({ hasError: false , error: null });
};
render () {
if ( this .state.hasError) {
const FallbackComponent = this .props.fallback || ErrorFallback;
return (
< FallbackComponent
error = { this .state.error}
resetError = { this .resetError}
/>
);
}
return this .props.children;
}
}
// Usage with custom fallback
const App = () => (
< ErrorBoundaryWithFallback
fallback = {CustomErrorPage}
onError = {( error , info ) => logToService (error, info)}
>
< Routes />
</ ErrorBoundaryWithFallback >
);
// Component that will error
const BuggyComponent = () => {
const [ count , setCount ] = useState ( 0 );
if (count > 3 ) {
throw new Error ( 'Count exceeded limit!' );
}
return (
< button onClick = {() => setCount (count + 1 )}>
Count: {count}
</ button >
);
};
Warning: Error boundaries do NOT catch errors in event handlers, async code (setTimeout,
promises),
server-side rendering, or errors thrown in the error boundary itself. Use try/catch for those cases.
11.2 componentDidCatch and getDerivedStateFromError
Method
When Called
Purpose
Can Update State
getDerivedStateFromError
During render phase
Update state to show fallback UI
✅ Yes (return new state)
componentDidCatch
After commit phase
Log errors, report to service
⚠️ Yes but triggers extra render
Parameter
Available In
Contains
error
Both methods
Error object with message, stack
errorInfo
componentDidCatch only
componentStack - where error occurred
Example: Using both lifecycle methods
class AdvancedErrorBoundary extends React . Component {
constructor ( props ) {
super (props);
this .state = {
hasError: false ,
error: null ,
errorInfo: null ,
errorCount: 0
};
}
// Static method - called during render phase
// MUST be pure - no side effects!
static getDerivedStateFromError ( error ) {
// Update state to trigger fallback UI
return {
hasError: true ,
error: error
};
}
// Called after render in commit phase
// Can have side effects - logging, reporting
componentDidCatch ( error , errorInfo ) {
// errorInfo.componentStack shows component hierarchy
console. error ( 'Error caught:' , error);
console. error ( 'Component stack:' , errorInfo.componentStack);
// Update state to store error details
this . setState ( prevState => ({
errorInfo,
errorCount: prevState.errorCount + 1
}));
// Report to error tracking service
if ( this .props.onError) {
this .props. onError (error, errorInfo);
}
// Send to monitoring service
// Sentry.captureException(error, { contexts: { react: errorInfo } });
}
componentDidUpdate ( prevProps , prevState ) {
// Clear error if children change (new route, etc)
if ( this .state.hasError && prevProps.children !== this .props.children) {
this . setState ({ hasError: false , error: null , errorInfo: null });
}
}
handleReset = () => {
this . setState ({
hasError: false ,
error: null ,
errorInfo: null
});
};
render () {
if ( this .state.hasError) {
return (
< div className = "error-boundary" >
< h1 >Something went wrong</ h1 >
< p >{ this .state.error?.message}</ p >
< p >Error count: { this .state.errorCount}</ p >
{process.env. NODE_ENV === 'development' && (
<>
< h3 >Error Stack:</ h3 >
< pre >{ this .state.error?.stack}</ pre >
< h3 >Component Stack:</ h3 >
< pre >{ this .state.errorInfo?.componentStack}</ pre >
</>
)}
< button onClick = { this .handleReset}>
Try Again
</ button >
</ div >
);
}
return this .props.children;
}
}
// Error boundary with different fallbacks based on error type
class SmartErrorBoundary extends React . Component {
constructor ( props ) {
super (props);
this .state = { hasError: false , error: null };
}
static getDerivedStateFromError ( error ) {
return { hasError: true , error };
}
componentDidCatch ( error , errorInfo ) {
// Categorize errors
const errorType = this . categorizeError (error);
console. error (\ `[ \$ {errorType}] Error:` , error);
console. error ( 'Stack:' , errorInfo.componentStack);
// Report with category
this . reportError (error, errorInfo, errorType);
}
categorizeError ( error ) {
if (error.message. includes ( 'fetch' )) return 'NETWORK' ;
if (error.message. includes ( 'undefined' )) return 'RUNTIME' ;
if (error.name === 'ChunkLoadError' ) return 'CHUNK_LOAD' ;
return 'UNKNOWN' ;
}
reportError ( error , errorInfo , category ) {
// Send to monitoring service
fetch ( '/api/errors' , {
method: 'POST' ,
body: JSON . stringify ({
message: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
category,
timestamp: Date. now (),
userAgent: navigator.userAgent
})
});
}
render () {
if ( this .state.hasError) {
const errorType = this . categorizeError ( this .state.error);
// Different UI based on error type
switch (errorType) {
case 'NETWORK' :
return < NetworkErrorFallback />;
case 'CHUNK_LOAD' :
return < ChunkLoadErrorFallback />;
default :
return < GenericErrorFallback error = { this .state.error} />;
}
}
return this .props.children;
}
}
Note: Use getDerivedStateFromError for UI updates (pure), and
componentDidCatch for side effects like logging. getDerivedStateFromError is called during
render, so it must be pure.
11.3 useErrorHandler Hook Patterns
Pattern
Description
Use Case
Custom hook
Throw errors to boundary from hooks
Event handlers, async operations
Error state
Store error in state, throw in render
Propagate to error boundary
Library (react-error-boundary)
Pre-built hooks and components
Production-ready solution
Example: useErrorHandler custom hook
// Custom useErrorHandler hook
const useErrorHandler = () => {
const [ error , setError ] = useState ( null );
// Throw error during render to trigger error boundary
if (error) {
throw error;
}
return setError;
};
// Usage in event handlers
const MyComponent = () => {
const handleError = useErrorHandler ();
const handleClick = async () => {
try {
await somethingThatMightFail ();
} catch (error) {
handleError (error); // Propagates to error boundary
}
};
return < button onClick = {handleClick}>Click me</ button >;
};
// Using with data fetching
const UserProfile = ({ userId }) => {
const [ user , setUser ] = useState ( null );
const handleError = useErrorHandler ();
useEffect (() => {
const fetchUser = async () => {
try {
const response = await fetch (\ `/api/users/ \$ {userId} \` );
if (!response.ok) throw new Error('Failed to fetch user');
const data = await response.json();
setUser(data);
} catch (error) {
handleError(error); // Triggers error boundary
}
};
fetchUser();
}, [userId, handleError]);
if (!user) return <div>Loading...</div>;
return <div>{user.name}</div>;
};
// Using react-error-boundary library
import { useErrorHandler, ErrorBoundary } from 'react-error-boundary';
const DataFetcher = () => {
const handleError = useErrorHandler();
useEffect(() => {
fetchData()
.catch(handleError); // Automatically propagates to boundary
}, [handleError]);
return <div>Data</div>;
};
// Error boundary with hook from library
const App = () => (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, errorInfo) => {
console.error('Error:', error);
console.error('Info:', errorInfo);
}}
onReset={() => {
// Reset app state
window.location.href = '/';
}}
>
<DataFetcher />
</ErrorBoundary>
);
// Advanced hook with error context
const ErrorContext = createContext();
const ErrorProvider = ({ children }) => {
const [errors, setErrors] = useState([]);
const addError = useCallback((error) => {
setErrors(prev => [...prev, {
id: Date.now(),
message: error.message,
timestamp: new Date()
}]);
}, []);
const clearErrors = useCallback(() => {
setErrors([]);
}, []);
return (
<ErrorContext.Provider value={{ errors, addError, clearErrors }}>
{children}
</ErrorContext.Provider>
);
};
const useError = () => {
const context = useContext(ErrorContext);
if (!context) {
throw new Error('useError must be used within ErrorProvider');
}
return context;
};
// Usage
const MyComponent = () => {
const { addError, errors } = useError();
const handleAction = async () => {
try {
await riskyOperation();
} catch (error) {
addError(error);
}
};
return (
<div>
{errors.map(err => (
<div key={err.id} className="error-toast">
{err.message}
</div>
))}
<button onClick={handleAction}>Action</button>
</div>
);
};
// Retrying with hook
const useAsyncError = () => {
const [, setError] = useState();
return useCallback(
(error) => {
setError(() => {
throw error;
});
},
[]
);
};
const ComponentWithRetry = () => {
const throwError = useAsyncError();
const [retryCount, setRetryCount] = useState(0);
const fetchData = async () => {
try {
const data = await fetch('/api/data');
if (!data.ok) throw new Error('Fetch failed');
return data.json();
} catch (error) {
if (retryCount < 3) {
setRetryCount(retryCount + 1);
} else {
throwError(error);
}
}
};
useEffect(() => {
fetchData();
}, [retryCount]);
return <div>Content</div>;
};
Note: Consider using the react-error-boundary library for production apps.
It provides battle-tested hooks and components with advanced features like retry, reset keys, and more.
11.4 Async Error Handling and Promise Rejections
Async Scenario
Error Boundary?
Solution
useEffect fetch
❌ No
try/catch + error state or hook
Event handler async
❌ No
try/catch + error state
setTimeout/setInterval
❌ No
try/catch in callback
Promise.catch
❌ No
.catch() or try/catch with await
Unhandled rejection
❌ No
Global handler + error boundary
Example: Async error handling patterns
// Async/await with try/catch
const DataComponent = () => {
const [ data , setData ] = useState ( null );
const [ error , setError ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
const fetchData = async () => {
try {
setLoading ( true );
setError ( null );
const response = await fetch ( '/api/data' );
if ( ! response.ok) {
throw new Error (\ `HTTP error! status: \$ {response.status} \` );
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
console.error('Fetch error:', err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Promise chains with .catch()
const PromiseChainComponent = () => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
fetch('/api/data')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => setData(data))
.catch(error => {
console.error('Error:', error);
setError(error.message);
});
}, []);
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
};
// Global unhandled rejection handler
const setupGlobalErrorHandling = () => {
// Unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Prevent default browser handling
event.preventDefault();
// Report to monitoring service
reportError({
type: 'unhandled_rejection',
error: event.reason,
promise: event.promise
});
});
// Global error handler
window.addEventListener('error', (event) => {
console.error('Global error:', event.error);
reportError({
type: 'global_error',
error: event.error,
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
};
// Call in app initialization
setupGlobalErrorHandling();
// Async error with custom hook
const useAsyncOperation = (asyncFn) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const execute = useCallback(async (...args) => {
try {
setLoading(true);
setError(null);
const result = await asyncFn(...args);
setData(result);
return result;
} catch (err) {
setError(err);
throw err; // Re-throw if caller wants to handle
} finally {
setLoading(false);
}
}, [asyncFn]);
return { data, error, loading, execute };
};
// Usage
const MyComponent = () => {
const { data, error, loading, execute } = useAsyncOperation(
async (id) => {
const response = await fetch( \` /api/users/ \$ {id} \` );
if (!response.ok) throw new Error('Fetch failed');
return response.json();
}
);
const handleClick = async () => {
try {
await execute(123);
} catch (err) {
console.error('Operation failed:', err);
}
};
return (
<div>
<button onClick={handleClick} disabled={loading}>
Fetch User
</button>
{loading && <div>Loading...</div>}
{error && <div>Error: {error.message}</div>}
{data && <div>{data.name}</div>}
</div>
);
};
// Timeout with error handling
const ComponentWithTimeout = () => {
const [message, setMessage] = useState('');
const handleError = useErrorHandler();
useEffect(() => {
const timerId = setTimeout(() => {
try {
// Risky operation
riskyOperation();
setMessage('Success');
} catch (error) {
handleError(error);
}
}, 1000);
return () => clearTimeout(timerId);
}, [handleError]);
return <div>{message}</div>;
};
// Parallel requests with error handling
const MultipleRequests = () => {
const [data, setData] = useState(null);
const [errors, setErrors] = useState([]);
useEffect(() => {
const fetchAll = async () => {
const urls = ['/api/data1', '/api/data2', '/api/data3'];
// Promise.allSettled to handle partial failures
const results = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
setData(successful);
setErrors(failed);
};
fetchAll();
}, []);
return (
<div>
{errors.length > 0 && (
<div>
{errors.length} requests failed
</div>
)}
{data && <div>{JSON.stringify(data)}</div>}
</div>
);
};
Warning: Always handle promise rejections! Unhandled rejections can cause silent failures.
Use try/catch with async/await or .catch() with promises. Set up global handlers as a safety net.
11.5 Error Reporting and Monitoring Integration
Service
Features
Integration
Sentry
Error tracking, performance, releases
@sentry/react SDK
LogRocket
Session replay, error tracking
logrocket + logrocket-react
Bugsnag
Error monitoring, releases
@bugsnag/js + @bugsnag/plugin-react
Rollbar
Real-time error tracking
rollbar package
Example: Error monitoring integration
// Sentry integration
import * as Sentry from '@sentry/react' ;
// Initialize Sentry
Sentry. init ({
dsn: 'YOUR_SENTRY_DSN' ,
environment: process.env. NODE_ENV ,
release: process.env. REACT_APP_VERSION ,
tracesSampleRate: 1.0 , // Capture 100% of transactions
integrations: [
new Sentry. BrowserTracing (),
new Sentry. Replay ()
],
beforeSend ( event , hint ) {
// Modify or filter events before sending
if (event.exception) {
console. error ( 'Sending error to Sentry:' , hint.originalException);
}
return event;
}
});
// Use Sentry's error boundary
const App = () => (
< Sentry.ErrorBoundary
fallback = {({ error , resetError }) => (
< div >
< h1 >An error occurred</ h1 >
< p >{error.message}</ p >
< button onClick = {resetError}>Try again</ button >
</ div >
)}
showDialog
>
< Routes />
</ Sentry.ErrorBoundary >
);
// Manual error capture
const handleError = ( error ) => {
Sentry. captureException (error, {
tags: {
component: 'UserProfile' ,
action: 'fetchUser'
},
level: 'error' ,
extra: {
userId: user.id,
timestamp: Date. now ()
}
});
};
// Add user context
Sentry. setUser ({
id: user.id,
email: user.email,
username: user.username
});
// Add breadcrumbs for debugging
Sentry. addBreadcrumb ({
category: 'user-action' ,
message: 'User clicked submit button' ,
level: 'info'
});
// LogRocket integration
import LogRocket from 'logrocket' ;
import setupLogRocketReact from 'logrocket-react' ;
// Initialize LogRocket
LogRocket. init ( 'your-app-id/project-name' , {
release: process.env. REACT_APP_VERSION ,
console: {
shouldAggregateConsoleErrors: true
}
});
setupLogRocketReact (LogRocket);
// Identify users
LogRocket. identify (user.id, {
name: user.name,
email: user.email,
subscriptionType: user.plan
});
// LogRocket with Sentry
LogRocket. getSessionURL (( sessionURL ) => {
Sentry. configureScope (( scope ) => {
scope. setExtra ( 'sessionURL' , sessionURL);
});
});
// Custom error reporting service
class ErrorReporter {
static async report ( error , errorInfo , context = {}) {
const errorData = {
message: error.message,
stack: error.stack,
componentStack: errorInfo?.componentStack,
context,
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date (). toISOString (),
user: this . getUserContext ()
};
try {
await fetch ( '/api/errors' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify (errorData)
});
} catch (reportError) {
console. error ( 'Failed to report error:' , reportError);
}
}
static getUserContext () {
// Get user info from your auth system
return {
id: localStorage. getItem ( 'userId' ),
sessionId: localStorage. getItem ( 'sessionId' )
};
}
}
// Use in error boundary
class MonitoredErrorBoundary extends React . Component {
state = { hasError: false };
static getDerivedStateFromError ( error ) {
return { hasError: true };
}
componentDidCatch ( error , errorInfo ) {
// Report to multiple services
ErrorReporter. report (error, errorInfo, {
component: this .props.name
});
Sentry. captureException (error, {
contexts: { react: errorInfo }
});
LogRocket. captureException (error, {
extra: errorInfo
});
}
render () {
if ( this .state.hasError) {
return < ErrorFallback />;
}
return this .props.children;
}
}
// Automatic error reporting with fetch interceptor
const originalFetch = window.fetch;
window. fetch = async ( ... args ) => {
try {
const response = await originalFetch ( ... args);
if ( ! response.ok) {
Sentry. captureMessage (\ `HTTP Error: \$ {response.status} \` , {
level: 'warning',
extra: {
url: args[0],
status: response.status
}
});
}
return response;
} catch (error) {
Sentry.captureException(error, {
tags: { type: 'fetch-error' }
});
throw error;
}
};
Note: Choose an error monitoring service that fits your needs. Sentry is popular for error
tracking, LogRocket adds session replay, and both provide valuable debugging context. Always sanitize sensitive
data before reporting.
11.6 Fallback UI Components and Recovery Patterns
Pattern
Description
User Experience
Full page fallback
Replace entire app with error page
Critical errors only
Section fallback
Replace section with error message
Rest of app still works
Inline error
Show error inline in component
Minimal disruption
Retry button
Let user retry failed operation
Self-service recovery
Automatic retry
Retry with exponential backoff
Seamless recovery attempt
Graceful degradation
Show partial content/reduced features
Better than nothing
Example: Fallback UI and recovery patterns
// Simple error fallback
const ErrorFallback = ({ error , resetError }) => (
< div className = "error-container" >
< h2 >⚠️ Something went wrong</ h2 >
< p >{error.message}</ p >
< button onClick = {resetError}>Try again</ button >
</ div >
);
// Feature-specific fallback
const FeatureFallback = ({ error , resetError , featureName }) => (
< div className = "feature-error" >
< p >Unable to load {featureName}</ p >
< button onClick = {resetError}>Retry</ button >
< button onClick = {() => window.location. reload ()}>
Reload Page
</ button >
</ div >
);
// Graceful degradation example
const DataList = ({ items }) => {
const [ error , setError ] = useState ( null );
const [ enhancedData , setEnhancedData ] = useState ( null );
useEffect (() => {
// Try to enhance data with additional info
const enhance = async () => {
try {
const enhanced = await fetchEnhancedData (items);
setEnhancedData (enhanced);
} catch (err) {
setError (err);
// Continue with basic data
}
};
enhance ();
}, [items]);
// Show basic data if enhancement fails
const displayData = enhancedData || items;
return (
< div >
{error && (
< div className = "warning" >
Showing basic view (enhanced features unavailable)
</ div >
)}
{displayData. map ( item => (
< div key = {item.id}>{item.name}</ div >
))}
</ div >
);
};
// Error boundary with reset key
const App = () => {
const [ resetKey , setResetKey ] = useState ( 0 );
return (
< ErrorBoundary
resetKey = {resetKey} // Changes when state updates
onReset = {() => setResetKey ( k => k + 1 )}
FallbackComponent = {ErrorFallback}
>
< Routes />
</ ErrorBoundary >
);
};
// Automatic retry with exponential backoff
const useRetryableAsync = ( asyncFn , maxRetries = 3 ) => {
const [ state , setState ] = useState ({
data: null ,
error: null ,
loading: false ,
retryCount: 0
});
const execute = useCallback ( async ( ... args ) => {
setState ( s => ({ ... s, loading: true , error: null }));
for ( let i = 0 ; i <= maxRetries; i ++ ) {
try {
const result = await asyncFn ( ... args);
setState ({
data: result,
error: null ,
loading: false ,
retryCount: i
});
return result;
} catch (error) {
if (i === maxRetries) {
setState ({
data: null ,
error,
loading: false ,
retryCount: i
});
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delay = Math. pow ( 2 , i) * 1000 ;
await new Promise ( resolve => setTimeout (resolve, delay));
}
}
}, [asyncFn, maxRetries]);
return { ... state, execute };
};
// Usage
const DataComponent = () => {
const { data , error , loading , retryCount , execute } = useRetryableAsync (
async () => {
const response = await fetch ( '/api/data' );
if ( ! response.ok) throw new Error ( 'Fetch failed' );
return response. json ();
}
);
useEffect (() => {
execute ();
}, [execute]);
if (loading) return < div >Loading... (attempt {retryCount + 1 }/3)</ div >;
if (error) return < div >Failed after {retryCount} retries</ div >;
return < div >{ JSON . stringify (data)}</ div >;
};
// Chunk load error recovery (code splitting)
const ChunkLoadErrorFallback = () => (
< div className = "chunk-error" >
< h2 >Update Available</ h2 >
< p >A new version of the app is available.</ p >
< button onClick = {() => window.location. reload ()}>
Reload to Update
</ button >
</ div >
);
// Network error fallback
const NetworkErrorFallback = ({ resetError }) => (
< div className = "network-error" >
< h2 >🌐 Connection Issue</ h2 >
< p >Please check your internet connection.</ p >
< button onClick = {resetError}>Retry</ button >
</ div >
);
// Multiple error boundaries for granular fallbacks
const Dashboard = () => (
< div >
< Header /> { /* Outside boundaries - always shown */ }
< div className = "dashboard-content" >
< ErrorBoundary FallbackComponent = {SidebarError}>
< Sidebar />
</ ErrorBoundary >
< ErrorBoundary FallbackComponent = {MainContentError}>
< MainContent />
</ ErrorBoundary >
< ErrorBoundary FallbackComponent = {WidgetError}>
< Widgets />
</ ErrorBoundary >
</ div >
</ div >
);
12. Refs and DOM Manipulation
12.1 useRef Hook for DOM Element Access
Concept
Description
Use Case
useRef Hook
Creates mutable ref object with .current property that persists across renders
DOM access, storing mutable values, keeping previous values
DOM Ref
Reference to actual DOM element when assigned via ref attribute
Focus management, scroll control, measuring elements
Ref Stability
Ref object remains the same across renders, only .current changes
Avoid re-renders when storing mutable values
No Re-render
Changing ref.current doesn't trigger component re-render
Storing timers, intervals, subscriptions
Example: Basic DOM Element Access
import { useRef, useEffect } from 'react' ;
function AutoFocusInput () {
const inputRef = useRef ( null );
useEffect (() => {
// Access DOM element after mount
if (inputRef.current) {
inputRef.current. focus ();
}
}, []);
const handleClear = () => {
if (inputRef.current) {
inputRef.current.value = '' ;
inputRef.current. focus ();
}
};
return (
< div >
< input ref = {inputRef} type = "text" />
< button onClick = {handleClear}>Clear</ button >
</ div >
);
}
Example: Multiple Refs with Array
function MultipleInputs () {
const inputRefs = useRef ([]);
const focusInput = ( index ) => {
inputRefs.current[index]?. focus ();
};
return (
< div >
{[ 0 , 1 , 2 ]. map (( index ) => (
< input
key = {index}
ref = {( el ) => inputRefs.current[index] = el}
onKeyPress = {( e ) => {
if (e.key === 'Enter' && index < 2 ) {
focusInput (index + 1 );
}
}}
/>
))}
</ div >
);
}
Example: Storing Mutable Values
function Timer () {
const [ count , setCount ] = useState ( 0 );
const intervalRef = useRef ( null );
const renderCountRef = useRef ( 0 );
useEffect (() => {
// Track render count without causing re-renders
renderCountRef.current += 1 ;
});
const startTimer = () => {
if ( ! intervalRef.current) {
intervalRef.current = setInterval (() => {
setCount (( c ) => c + 1 );
}, 1000 );
}
};
const stopTimer = () => {
if (intervalRef.current) {
clearInterval (intervalRef.current);
intervalRef.current = null ;
}
};
useEffect (() => {
return () => stopTimer (); // Cleanup on unmount
}, []);
return (
< div >
< p >Count: {count}</ p >
< p >Renders: {renderCountRef.current}</ p >
< button onClick = {startTimer}>Start</ button >
< button onClick = {stopTimer}>Stop</ button >
</ div >
);
}
Ref Pattern
Syntax
When to Use
Initial Value
useRef(initialValue)
Set .current to initial value (useful for non-DOM refs)
Null DOM Ref
useRef(null)
Standard pattern for DOM element refs
Callback Ref
ref={(el) => ...}
Need to run code when ref attaches/detaches
Conditional Ref
ref={condition ? myRef : null}
Conditionally attach ref
12.2 forwardRef and Ref Forwarding Patterns
Concept
Description
Use Case
forwardRef
HOC that allows component to receive and pass ref to child element
Custom components that need to expose DOM element
Ref Forwarding
Technique to automatically pass ref through component to child
Reusable UI components (Input, Button wrappers)
Display Name
Set component.displayName for better debugging with forwardRef
DevTools shows meaningful names
Generic Refs
TypeScript generic type for typed ref forwarding
Type-safe ref forwarding in TS
Example: Basic Ref Forwarding
import { forwardRef } from 'react' ;
// Without forwardRef - Won't work
function BrokenInput ( props ) {
return < input { ... props} />; // ref prop not forwarded
}
// With forwardRef - Works correctly
const CustomInput = forwardRef (( props , ref ) => {
return < input ref = {ref} { ... props} />;
});
CustomInput.displayName = 'CustomInput' ;
// Usage
function Form () {
const inputRef = useRef ( null );
const focusInput = () => {
inputRef.current?. focus ();
};
return (
< div >
< CustomInput ref = {inputRef} placeholder = "Enter text" />
< button onClick = {focusInput}>Focus Input</ button >
</ div >
);
}
Example: Styled Component with Ref Forwarding
const FancyButton = forwardRef (({ children , variant = 'primary' , ... props }, ref ) => {
const className = `btn btn-${ variant }` ;
return (
< button ref = {ref} className = {className} { ... props}>
{children}
</ button >
);
});
FancyButton.displayName = 'FancyButton' ;
// Usage with ref
function App () {
const btnRef = useRef ( null );
const measureButton = () => {
if (btnRef.current) {
const { width , height } = btnRef.current. getBoundingClientRect ();
console. log ( `Button size: ${ width }x${ height }` );
}
};
return (
< FancyButton ref = {btnRef} variant = "success" onClick = {measureButton}>
Click Me
</ FancyButton >
);
}
Example: Combining Own Ref with Forwarded Ref
const InputWithValidation = forwardRef (({ onValidate , ... props }, forwardedRef ) => {
const internalRef = useRef ( null );
// Combine both refs
useEffect (() => {
if (forwardedRef) {
if ( typeof forwardedRef === 'function' ) {
forwardedRef (internalRef.current);
} else {
forwardedRef.current = internalRef.current;
}
}
}, [forwardedRef]);
const handleBlur = () => {
if (internalRef.current && onValidate) {
onValidate (internalRef.current.value);
}
};
return (
< input
ref = {internalRef}
onBlur = {handleBlur}
{ ... props}
/>
);
});
InputWithValidation.displayName = 'InputWithValidation' ;
Example: TypeScript Ref Forwarding
import { forwardRef, Ref } from 'react' ;
interface InputProps {
label : string ;
error ?: string ;
}
const FormInput = forwardRef < HTMLInputElement , InputProps >(
({ label , error , ... props }, ref ) => {
return (
< div className = "form-group" >
< label >{label}</ label >
< input ref = {ref} { ... props} />
{error && < span className = "error" >{error}</ span >}
</ div >
);
}
);
FormInput.displayName = 'FormInput' ;
// Usage with type safety
function TypedForm () {
const inputRef = useRef < HTMLInputElement >( null );
return < FormInput ref = {inputRef} label = "Email" />;
}
Ref Forwarding Best Practices:
Always set displayName for better DevTools experience
Use forwardRef for reusable UI components
Document which DOM element the ref points to
Consider using useImperativeHandle for custom APIs
Handle both function and object refs in custom logic
12.3 useImperativeHandle for Custom Ref APIs
Concept
Description
Use Case
useImperativeHandle
Customizes ref instance value exposed to parent components
Expose limited, controlled API instead of full DOM element
Ref API
Custom object with methods/properties exposed via ref
Hide implementation, provide semantic methods
Encapsulation
Hide internal DOM structure, expose only intended interface
Complex components with internal state/refs
Dependencies
Third parameter array, similar to useEffect
Recreate handle when dependencies change
import { forwardRef, useRef, useImperativeHandle } from 'react' ;
const AdvancedInput = forwardRef (( props , ref ) => {
const inputRef = useRef ( null );
const [ value , setValue ] = useState ( '' );
// Expose custom API instead of raw DOM element
useImperativeHandle (ref, () => ({
// Custom methods
focus : () => {
inputRef.current?. focus ();
},
clear : () => {
setValue ( '' );
inputRef.current?. focus ();
},
getValue : () => value,
setValue : ( newValue ) => setValue (newValue),
// Expose select DOM methods
scrollIntoView : () => {
inputRef.current?. scrollIntoView ({ behavior: 'smooth' });
}
}), [value]); // Recreate when value changes
return (
< input
ref = {inputRef}
value = {value}
onChange = {( e ) => setValue (e.target.value)}
{ ... props}
/>
);
});
// Usage
function Form () {
const inputRef = useRef ( null );
const handleSubmit = () => {
const value = inputRef.current?. getValue ();
console. log ( 'Value:' , value);
inputRef.current?. clear ();
};
return (
< div >
< AdvancedInput ref = {inputRef} />
< button onClick = {handleSubmit}>Submit</ button >
</ div >
);
}
Example: Modal Component with Imperative API
const Modal = forwardRef (({ children }, ref ) => {
const [ isOpen , setIsOpen ] = useState ( false );
const [ content , setContent ] = useState ( null );
const modalRef = useRef ( null );
useImperativeHandle (ref, () => ({
open : ( newContent ) => {
setContent (newContent);
setIsOpen ( true );
},
close : () => {
setIsOpen ( false );
},
toggle : () => {
setIsOpen (( prev ) => ! prev);
},
isOpen : () => isOpen,
updateContent : ( newContent ) => {
setContent (newContent);
}
}), [isOpen]);
if ( ! isOpen) return null ;
return (
< div className = "modal-overlay" onClick = {() => setIsOpen ( false )}>
< div
ref = {modalRef}
className = "modal-content"
onClick = {( e ) => e. stopPropagation ()}
>
{content || children}
</ div >
</ div >
);
});
// Usage
function App () {
const modalRef = useRef ( null );
const showSuccess = () => {
modalRef.current?. open (< div >Success!</ div >);
setTimeout (() => modalRef.current?. close (), 2000 );
};
return (
< div >
< button onClick = {showSuccess}>Show Modal</ button >
< Modal ref = {modalRef} />
</ div >
);
}
Example: Video Player with Imperative Controls
const VideoPlayer = forwardRef (({ src }, ref ) => {
const videoRef = useRef ( null );
useImperativeHandle (ref, () => ({
play : () => videoRef.current?. play (),
pause : () => videoRef.current?. pause (),
stop : () => {
if (videoRef.current) {
videoRef.current. pause ();
videoRef.current.currentTime = 0 ;
}
},
seek : ( time ) => {
if (videoRef.current) {
videoRef.current.currentTime = time;
}
},
setVolume : ( volume ) => {
if (videoRef.current) {
videoRef.current.volume = Math. max ( 0 , Math. min ( 1 , volume));
}
},
getCurrentTime : () => videoRef.current?.currentTime ?? 0 ,
getDuration : () => videoRef.current?.duration ?? 0 ,
isPlaying : () => ! videoRef.current?.paused
}));
return < video ref = {videoRef} src = {src} />;
});
// Usage
function VideoControls () {
const playerRef = useRef ( null );
const handleSkip = ( seconds ) => {
const current = playerRef.current?. getCurrentTime () ?? 0 ;
playerRef.current?. seek (current + seconds);
};
return (
< div >
< VideoPlayer ref = {playerRef} src = "video.mp4" />
< button onClick = {() => playerRef.current?. play ()}>Play</ button >
< button onClick = {() => playerRef.current?. pause ()}>Pause</ button >
< button onClick = {() => handleSkip ( - 10 )}>-10s</ button >
< button onClick = {() => handleSkip ( 10 )}>+10s</ button >
</ div >
);
}
Pattern
Description
Example
Action Methods
Methods that perform actions (focus, clear, submit)
{ focus: () => {...}, clear: () => {...} }
Query Methods
Methods that return values (getValue, isValid)
{ getValue: () => value, isValid: () => valid }
State Methods
Methods that change internal state (open, close)
{ open: () => {...}, close: () => {...} }
Controlled API
Expose subset of DOM methods with validation
{ play: () => video.play(), pause: () => video.pause() }
useImperativeHandle Caveats:
Use sparingly - prefer controlled components and props
Don't expose full DOM element unless necessary
Document the imperative API thoroughly
Consider dependencies array to avoid stale closures
Overuse breaks React's declarative paradigm
12.4 Focus Management and Accessibility
Technique
Description
Accessibility Impact
Auto Focus
Automatically focus element on mount or condition
Improves keyboard navigation, required for modals
Focus Trap
Keep focus within container (modal, dropdown)
Prevents focus from escaping modal dialogs
Focus Return
Return focus to trigger element after modal closes
Maintains user's place in navigation flow
Skip Links
Allow keyboard users to skip navigation
WCAG 2.1 Level A requirement
Focus Indicators
Visible outline/highlight for focused elements
Required for keyboard-only users
Tab Order
Control tabIndex for logical navigation
Ensures logical focus flow
Example: Auto Focus on Mount
function LoginForm () {
const usernameRef = useRef ( null );
useEffect (() => {
// Focus first input on mount
usernameRef.current?. focus ();
}, []);
return (
< form >
< input ref = {usernameRef} name = "username" placeholder = "Username" />
< input name = "password" type = "password" placeholder = "Password" />
< button type = "submit" >Login</ button >
</ form >
);
}
Example: Focus Trap in Modal
function Modal ({ isOpen , onClose , children }) {
const modalRef = useRef ( null );
const previousFocusRef = useRef ( null );
useEffect (() => {
if (isOpen) {
// Save current focus
previousFocusRef.current = document.activeElement;
// Focus modal
modalRef.current?. focus ();
// Setup focus trap
const handleKeyDown = ( e ) => {
if (e.key === 'Tab' ) {
const focusableElements = modalRef.current?. querySelectorAll (
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if ( ! focusableElements?. length ) return ;
const firstElement = focusableElements[ 0 ];
const lastElement = focusableElements[focusableElements. length - 1 ];
if (e.shiftKey) {
// Shift+Tab
if (document.activeElement === firstElement) {
lastElement. focus ();
e. preventDefault ();
}
} else {
// Tab
if (document.activeElement === lastElement) {
firstElement. focus ();
e. preventDefault ();
}
}
}
// Close on Escape
if (e.key === 'Escape' ) {
onClose ();
}
};
document. addEventListener ( 'keydown' , handleKeyDown);
return () => document. removeEventListener ( 'keydown' , handleKeyDown);
} else {
// Return focus when modal closes
previousFocusRef.current?. focus ();
}
}, [isOpen, onClose]);
if ( ! isOpen) return null ;
return (
< div className = "modal-overlay" >
< div
ref = {modalRef}
className = "modal"
role = "dialog"
aria-modal = "true"
tabIndex = { - 1 }
>
{children}
< button onClick = {onClose}>Close</ button >
</ div >
</ div >
);
}
Example: Skip Navigation Link
function PageLayout ({ children }) {
const mainContentRef = useRef ( null );
const skipToMain = ( e ) => {
e. preventDefault ();
mainContentRef.current?. focus ();
mainContentRef.current?. scrollIntoView ();
};
return (
<>
< a
href = "#main"
className = "skip-link"
onClick = {skipToMain}
style = {{
position: 'absolute' ,
left: '-9999px' ,
zIndex: 999
}}
onFocus = {( e ) => {
e.target.style.left = '0' ;
}}
onBlur = {( e ) => {
e.target.style.left = '-9999px' ;
}}
>
Skip to main content
</ a >
< nav >
{ /* Navigation items */ }
</ nav >
< main
ref = {mainContentRef}
id = "main"
tabIndex = { - 1 }
style = {{ outline: 'none' }}
>
{children}
</ main >
</>
);
}
Example: Focus Management in Dynamic Lists
function TodoList () {
const [ todos , setTodos ] = useState ([]);
const itemRefs = useRef ( new Map ());
const deleteTodo = ( id , index ) => {
setTodos (( prev ) => prev. filter (( t ) => t.id !== id));
// Focus next item or previous if last
setTimeout (() => {
const nextIndex = Math. min (index, todos. length - 2 );
const nextId = todos[nextIndex]?.id;
if (nextId) {
itemRefs.current. get (nextId)?. focus ();
}
}, 0 );
};
return (
< ul >
{todos. map (( todo , index ) => (
< li key = {todo.id}>
< span >{todo.text}</ span >
< button
ref = {( el ) => {
if (el) {
itemRefs.current. set (todo.id, el);
} else {
itemRefs.current. delete (todo.id);
}
}}
onClick = {() => deleteTodo (todo.id, index)}
>
Delete
</ button >
</ li >
))}
</ ul >
);
}
ARIA Attribute
Purpose
Usage with Refs
aria-activedescendant
Identifies focused element in composite widget
Update with ref when selection changes
aria-describedby
References element describing current element
Link input to error message via ref IDs
aria-labelledby
References element(s) labeling current element
Associate labels with inputs using ref IDs
aria-live
Announces dynamic content changes
Focus live region ref for announcements
Focus Management Best Practices:
Always manage focus when showing/hiding modals
Return focus to trigger element after dialog closes
Implement focus trap for modal dialogs
Provide visible focus indicators (never use outline: none without replacement)
Test with keyboard-only navigation
Use semantic HTML elements for better focus behavior
Consider screen reader announcements with aria-live
Avoid unnecessary tabIndex values (rely on natural tab order)
12.5 DOM Measurements and Element Properties
Measurement
Property/Method
Use Case
Element Size
getBoundingClientRect()
Get size and position relative to viewport
Scroll Position
scrollTop, scrollLeft
Track/control scroll position
Offset Dimensions
offsetWidth, offsetHeight
Element dimensions including padding/border
Client Dimensions
clientWidth, clientHeight
Element dimensions excluding scrollbars
Scroll Dimensions
scrollWidth, scrollHeight
Total scrollable content size
Computed Styles
getComputedStyle()
Get actual CSS values applied
Example: Measuring Element Dimensions
function ElementMeasurements () {
const elementRef = useRef ( null );
const [ dimensions , setDimensions ] = useState ({});
useEffect (() => {
const measureElement = () => {
if (elementRef.current) {
const rect = elementRef.current. getBoundingClientRect ();
const computedStyle = window. getComputedStyle (elementRef.current);
setDimensions ({
// Position
top: rect.top,
left: rect.left,
right: rect.right,
bottom: rect.bottom,
// Size
width: rect.width,
height: rect.height,
// Offset dimensions (includes padding, border)
offsetWidth: elementRef.current.offsetWidth,
offsetHeight: elementRef.current.offsetHeight,
// Client dimensions (excludes scrollbar)
clientWidth: elementRef.current.clientWidth,
clientHeight: elementRef.current.clientHeight,
// Scroll dimensions
scrollWidth: elementRef.current.scrollWidth,
scrollHeight: elementRef.current.scrollHeight,
// Computed styles
marginTop: computedStyle.marginTop,
paddingLeft: computedStyle.paddingLeft,
});
}
};
measureElement ();
// Remeasure on resize
window. addEventListener ( 'resize' , measureElement);
return () => window. removeEventListener ( 'resize' , measureElement);
}, []);
return (
< div >
< div ref = {elementRef} style = {{ padding: '20px' , border: '2px solid' }}>
Measure me!
</ div >
< pre >{ JSON . stringify (dimensions, null , 2 )}</ pre >
</ div >
);
}
function ScrollTracker () {
const containerRef = useRef ( null );
const [ scrollInfo , setScrollInfo ] = useState ({
scrollTop: 0 ,
scrollPercentage: 0 ,
isAtBottom: false
});
const handleScroll = () => {
if (containerRef.current) {
const { scrollTop , scrollHeight , clientHeight } = containerRef.current;
const scrollPercentage = (scrollTop / (scrollHeight - clientHeight)) * 100 ;
const isAtBottom = scrollTop + clientHeight >= scrollHeight - 1 ;
setScrollInfo ({
scrollTop,
scrollPercentage: Math. round (scrollPercentage),
isAtBottom
});
}
};
const scrollToBottom = () => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
};
return (
< div >
< div >
< p >Scroll: {scrollInfo.scrollTop}px</ p >
< p >Progress: {scrollInfo.scrollPercentage}%</ p >
< p >At bottom: {scrollInfo.isAtBottom ? 'Yes' : 'No' }</ p >
</ div >
< div
ref = {containerRef}
onScroll = {handleScroll}
style = {{ height: '300px' , overflow: 'auto' }}
>
{ /* Long content */ }
</ div >
< button onClick = {scrollToBottom}>Scroll to Bottom</ button >
</ div >
);
}
Example: Intersection Observer with Refs
function LazyImage ({ src , alt }) {
const imgRef = useRef ( null );
const [ isVisible , setIsVisible ] = useState ( false );
const [ hasLoaded , setHasLoaded ] = useState ( false );
useEffect (() => {
const observer = new IntersectionObserver (
([ entry ]) => {
if (entry.isIntersecting) {
setIsVisible ( true );
observer. disconnect ();
}
},
{
rootMargin: '50px' , // Load slightly before visible
threshold: 0.01
}
);
if (imgRef.current) {
observer. observe (imgRef.current);
}
return () => observer. disconnect ();
}, []);
return (
< div ref = {imgRef} style = {{ minHeight: '200px' }}>
{isVisible && (
< img
src = {src}
alt = {alt}
onLoad = {() => setHasLoaded ( true )}
style = {{ opacity: hasLoaded ? 1 : 0 , transition: 'opacity 0.3s' }}
/>
)}
</ div >
);
}
Example: ResizeObserver for Responsive Components
function ResponsiveCard ({ children }) {
const cardRef = useRef ( null );
const [ size , setSize ] = useState ( 'medium' );
useEffect (() => {
const observer = new ResizeObserver (( entries ) => {
for ( const entry of entries) {
const width = entry.contentRect.width;
if (width < 300 ) {
setSize ( 'small' );
} else if (width < 600 ) {
setSize ( 'medium' );
} else {
setSize ( 'large' );
}
}
});
if (cardRef.current) {
observer. observe (cardRef.current);
}
return () => observer. disconnect ();
}, []);
return (
< div ref = {cardRef} className = { `card card-${ size }` }>
{children}
</ div >
);
}
Observer API
Purpose
Callback Timing
IntersectionObserver
Detect when element enters/exits viewport
When visibility changes
ResizeObserver
Detect when element size changes
When dimensions change
MutationObserver
Detect DOM tree changes
When DOM mutates
PerformanceObserver
Monitor performance metrics
When performance entries recorded
Performance Considerations:
Use IntersectionObserver instead of scroll event listeners
Use ResizeObserver instead of window resize + polling
Debounce/throttle measurements on scroll/resize when necessary
Read measurements in batches to avoid layout thrashing
Cache measurements when possible (recompute only on dependency change)
Disconnect observers when component unmounts
12.6 Third-party Library Integration with Refs
Library Type
Integration Pattern
Common Examples
DOM Libraries
Pass ref.current to library initialization
D3.js, Chart.js, Three.js
jQuery Plugins
Initialize plugin on ref element in useEffect
DataTables, Select2, jQuery UI
Maps
Provide container ref for map initialization
Leaflet, Google Maps, Mapbox
Rich Text Editors
Mount editor instance to ref element
TinyMCE, Quill, Monaco
Video Players
Initialize player with ref element
Video.js, Plyr, JW Player
Canvas Libraries
Get canvas element via ref for rendering
Fabric.js, Konva.js, Paper.js
Example: D3.js Chart Integration
import { useRef, useEffect } from 'react' ;
import * as d3 from 'd3' ;
function D3Chart ({ data }) {
const svgRef = useRef ( null );
useEffect (() => {
if ( ! svgRef.current || ! data) return ;
// Clear previous content
d3. select (svgRef.current). selectAll ( '*' ). remove ();
// Create chart
const svg = d3. select (svgRef.current);
const width = 500 ;
const height = 300 ;
const margin = { top: 20 , right: 20 , bottom: 30 , left: 40 };
const x = d3. scaleBand ()
. domain (data. map ( d => d.label))
. range ([margin.left, width - margin.right])
. padding ( 0.1 );
const y = d3. scaleLinear ()
. domain ([ 0 , d3. max (data, d => d.value)])
. nice ()
. range ([height - margin.bottom, margin.top]);
// Add bars
svg. selectAll ( 'rect' )
. data (data)
. join ( 'rect' )
. attr ( 'x' , d => x (d.label))
. attr ( 'y' , d => y (d.value))
. attr ( 'width' , x. bandwidth ())
. attr ( 'height' , d => y ( 0 ) - y (d.value))
. attr ( 'fill' , 'steelblue' );
// Add axes
svg. append ( 'g' )
. attr ( 'transform' , `translate(0,${ height - margin . bottom })` )
. call (d3. axisBottom (x));
svg. append ( 'g' )
. attr ( 'transform' , `translate(${ margin . left },0)` )
. call (d3. axisLeft (y));
}, [data]);
return < svg ref = {svgRef} width = { 500 } height = { 300 } />;
}
Example: Leaflet Map Integration
import { useRef, useEffect } from 'react' ;
import L from 'leaflet' ;
import 'leaflet/dist/leaflet.css' ;
function LeafletMap ({ center , zoom , markers }) {
const mapRef = useRef ( null );
const mapInstanceRef = useRef ( null );
useEffect (() => {
if ( ! mapRef.current) return ;
// Initialize map
if ( ! mapInstanceRef.current) {
mapInstanceRef.current = L . map (mapRef.current). setView (center, zoom);
L . tileLayer ( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' , {
attribution: '© OpenStreetMap contributors'
}). addTo (mapInstanceRef.current);
}
// Cleanup
return () => {
if (mapInstanceRef.current) {
mapInstanceRef.current. remove ();
mapInstanceRef.current = null ;
}
};
}, []);
// Update markers
useEffect (() => {
if ( ! mapInstanceRef.current) return ;
// Clear existing markers
mapInstanceRef.current. eachLayer (( layer ) => {
if (layer instanceof L . Marker ) {
mapInstanceRef.current. removeLayer (layer);
}
});
// Add new markers
markers?. forEach (( marker ) => {
L . marker (marker.position)
. addTo (mapInstanceRef.current)
. bindPopup (marker.popup);
});
}, [markers]);
return < div ref = {mapRef} style = {{ height: '400px' , width: '100%' }} />;
}
Example: Monaco Editor Integration
import { useRef, useEffect } from 'react' ;
import * as monaco from 'monaco-editor' ;
function CodeEditor ({ value , language , onChange }) {
const editorRef = useRef ( null );
const editorInstanceRef = useRef ( null );
useEffect (() => {
if ( ! editorRef.current) return ;
// Create editor
editorInstanceRef.current = monaco.editor. create (editorRef.current, {
value: value || '' ,
language: language || 'javascript' ,
theme: 'vs-dark' ,
automaticLayout: true ,
minimap: { enabled: false }
});
// Listen to changes
const disposable = editorInstanceRef.current. onDidChangeModelContent (() => {
const newValue = editorInstanceRef.current. getValue ();
onChange ?.(newValue);
});
// Cleanup
return () => {
disposable. dispose ();
editorInstanceRef.current?. dispose ();
};
}, []);
// Update value from prop
useEffect (() => {
if (editorInstanceRef.current && value !== undefined ) {
const currentValue = editorInstanceRef.current. getValue ();
if (currentValue !== value) {
editorInstanceRef.current. setValue (value);
}
}
}, [value]);
// Update language
useEffect (() => {
if (editorInstanceRef.current && language) {
const model = editorInstanceRef.current. getModel ();
monaco.editor. setModelLanguage (model, language);
}
}, [language]);
return < div ref = {editorRef} style = {{ height: '500px' , width: '100%' }} />;
}
Example: jQuery Plugin Wrapper
import { useRef, useEffect } from 'react' ;
import $ from 'jquery' ;
import 'select2' ; // jQuery plugin
import 'select2/dist/css/select2.css' ;
function Select2Component ({ options , value , onChange }) {
const selectRef = useRef ( null );
useEffect (() => {
if ( ! selectRef.current) return ;
// Initialize Select2
const $select = $ (selectRef.current);
$select. select2 ({
data: options,
width: '100%'
});
// Listen to changes
$select. on ( 'change' , ( e ) => {
onChange ?.(e.target.value);
});
// Cleanup
return () => {
$select. select2 ( 'destroy' );
$select. off ( 'change' );
};
}, [options]);
// Update value from prop
useEffect (() => {
if (selectRef.current && value !== undefined ) {
$ (selectRef.current). val (value). trigger ( 'change' );
}
}, [value]);
return (
< select ref = {selectRef}>
{options. map (( opt ) => (
< option key = {opt.id} value = {opt.id}>
{opt.text}
</ option >
))}
</ select >
);
}
Integration Challenge
Solution
Example
Library expects DOM element
Pass ref.current in useEffect after mount
D3.js, Leaflet initialization
Library manages own state
Sync library state with React state via callbacks
Editor onChange, map move events
Library mutates DOM
Don't render React children in library container
Keep library container div empty
Cleanup required
Call library destroy/dispose in useEffect cleanup
Monaco dispose(), Select2 destroy()
Props update
Update library instance in separate useEffect
Update map center, chart data
Instance methods needed
Store instance in ref, expose via useImperativeHandle
Map pan/zoom, editor formatting
Third-party Integration Best Practices:
Initialize library in useEffect with empty deps ([] = mount only)
Store library instance in ref for method calls
Always cleanup (destroy/dispose) in useEffect return
Sync React state to library in separate useEffect with deps
Don't let React and library both manage same DOM (choose one)
Wrap in custom component to encapsulate integration logic
Document which React patterns to avoid with library
Consider using React wrapper libraries when available
13. Advanced Component Patterns
13.1 Higher-Order Components (HOC) Patterns
HOC Pattern
Syntax
Description
Use Case
Basic HOC
withFeature(Component)
Function that takes component and returns enhanced component
Add props, wrap with context, inject dependencies
HOC with Arguments
withFeature(config)(Comp)
Curried HOC that accepts configuration
Configurable behavior, conditional enhancement
Props Proxy
props => <Comp {...props}/>
HOC manipulates props before passing to wrapped component
Transform, filter, or add props
Inheritance Inversion
class extends WrappedComp
HOC extends wrapped component class DEPRECATED
Modify lifecycle, render - avoid, use hooks instead
Composing HOCs
compose(hoc1, hoc2)(Comp)
Apply multiple HOCs in sequence
Combine multiple enhancements cleanly
Display Name
HOC.displayName = 'with...'
Set display name for debugging
Better DevTools, error messages, stack traces
Example: Basic HOC with authentication
// Basic HOC pattern
function withAuth ( Component ) {
return function AuthComponent ( props ) {
const { user , loading } = useAuth ();
if (loading) return < LoadingSpinner />;
if ( ! user) return < Navigate to = "/login" />;
return < Component { ... props} user = {user} />;
};
}
// Usage
const ProtectedDashboard = withAuth (Dashboard);
// HOC with configuration
function withPermission ( permission ) {
return function ( Component ) {
return function PermissionComponent ( props ) {
const { user } = useAuth ();
if ( ! user?.permissions. includes (permission)) {
return < AccessDenied />;
}
return < Component { ... props} />;
};
};
}
// Usage with config
const AdminPanel = withPermission ( 'admin' )(Panel);
// Props transformation HOC
function withUppercase ( Component ) {
return function UppercaseComponent ({ name , ... props }) {
return < Component { ... props} name = {name?. toUpperCase ()} />;
};
}
// Data fetching HOC
function withData ( fetchFn , propName ) {
return function ( Component ) {
return function DataComponent ( props ) {
const [ data , setData ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
fetchFn (). then (( result ) => {
setData (result);
setLoading ( false );
});
}, []);
return < Component { ... props} { ... {[propName]: data}} loading = {loading} />;
};
};
}
// Compose multiple HOCs
import { compose } from 'redux' ; // or lodash/fp
const enhance = compose (
withAuth,
withData (fetchUserProfile, 'profile' ),
withUppercase
);
const EnhancedProfile = enhance (ProfileComponent);
HOC Best Practices: Use descriptive names (with* prefix), pass all props through, set
displayName for debugging, prefer hooks for most cases, compose HOCs with utility functions, avoid mutating
wrapped component.
13.2 Render Props and Function-as-Children
Pattern
Syntax
Description
Use Case
Render Prop
<Comp render={data => ...}/>
Pass function as prop that returns React element
Share logic while giving render control to consumer
Children as Function
<Comp>{data => ...}</Comp>
Children prop is a function receiving data
More ergonomic API, follows React conventions
Multiple Render Props
header={...} footer={...}
Multiple function props for different render areas
Flexible layout with pluggable sections
Render Props with Hooks
useData().render()
Hooks replaced most render prop use cases PREFERRED
Modern alternative - cleaner, more composable
Example: Mouse tracker with render prop
// Render prop component
function MouseTracker ({ render }) {
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
useEffect (() => {
const handleMove = ( e ) => {
setPosition ({ x: e.clientX, y: e.clientY });
};
window. addEventListener ( 'mousemove' , handleMove);
return () => window. removeEventListener ( 'mousemove' , handleMove);
}, []);
return render (position);
}
// Usage with render prop
< MouseTracker
render = {({ x , y }) => (
< h1 >Mouse at {x}, {y}</ h1 >
)}
/>
// Children as function pattern
function DataProvider ({ url , children }) {
const [ data , loading , error ] = useFetch (url);
return children ({ data, loading, error });
}
// Usage
< DataProvider url = "/api/users" >
{({ data , loading , error }) => {
if (loading) return < Spinner />;
if (error) return < Error message = {error} />;
return < UserList users = {data} />;
}}
</ DataProvider >
Example: Modern hook alternative (preferred)
// Custom hook replaces render prop pattern
function useMouse () {
const [ position , setPosition ] = useState ({ x: 0 , y: 0 });
useEffect (() => {
const handleMove = ( e ) => {
setPosition ({ x: e.clientX, y: e.clientY });
};
window. addEventListener ( 'mousemove' , handleMove);
return () => window. removeEventListener ( 'mousemove' , handleMove);
}, []);
return position;
}
// Usage - much cleaner!
function MyComponent () {
const { x , y } = useMouse ();
return < h1 >Mouse at {x}, {y}</ h1 >;
}
Warning: Render props can hurt performance if function is recreated on each render. Use
useCallback for render prop functions. Consider custom hooks as modern alternative.
13.3 Compound Components and Component APIs
Pattern
Syntax
Description
Use Case
Compound Components
Menu.Item, Menu.Divider
Components designed to work together, share implicit state
Complex UI with coordinated subcomponents
Static Properties
Component.SubComponent
Attach child components as static properties of parent
Namespace related components, cleaner imports
Context-based
Context.Provider + useContext
Share state via context between parent and children
Flexible structure, works at any nesting level
Clone Element
React.cloneElement(child)
Clone and inject props into children
Add props to unknown children, less flexible
Controlled Compound
activeItem={...} onChange
Parent controls shared state externally
Integrate with forms, external state management
Example: Tabs compound component with context
// Context for shared state
const TabContext = createContext ();
function Tabs ({ children , defaultTab }) {
const [ activeTab , setActiveTab ] = useState (defaultTab);
return (
< TabContext.Provider value = {{ activeTab, setActiveTab }}>
< div className = "tabs" >{children}</ div >
</ TabContext.Provider >
);
}
function TabList ({ children }) {
return < div className = "tab-list" role = "tablist" >{children}</ div >;
}
function Tab ({ id , children }) {
const { activeTab , setActiveTab } = useContext (TabContext);
const isActive = activeTab === id;
return (
< button
role = "tab"
aria-selected = {isActive}
onClick = {() => setActiveTab (id)}
className = {isActive ? 'active' : '' }
>
{children}
</ button >
);
}
function TabPanel ({ id , children }) {
const { activeTab } = useContext (TabContext);
if (activeTab !== id) return null ;
return < div role = "tabpanel" >{children}</ div >;
}
// Attach as static properties
Tabs.List = TabList;
Tabs.Tab = Tab;
Tabs.Panel = TabPanel;
// Usage - declarative API
< Tabs defaultTab = "home" >
< Tabs.List >
< Tabs.Tab id = "home" >Home</ Tabs.Tab >
< Tabs.Tab id = "profile" >Profile</ Tabs.Tab >
< Tabs.Tab id = "settings" >Settings</ Tabs.Tab >
</ Tabs.List >
< Tabs.Panel id = "home" >Home content</ Tabs.Panel >
< Tabs.Panel id = "profile" >Profile content</ Tabs.Panel >
< Tabs.Panel id = "settings" >Settings content</ Tabs.Panel >
</ Tabs >
Example: Controlled compound component
// Controlled version for external state control
function ControlledTabs ({ children , activeTab , onTabChange }) {
return (
< TabContext.Provider value = {{ activeTab, setActiveTab: onTabChange }}>
< div className = "tabs" >{children}</ div >
</ TabContext.Provider >
);
}
// Usage with external state
function App () {
const [ tab , setTab ] = useState ( 'home' );
return (
<>
< button onClick = {() => setTab ( 'profile' )}>
Go to Profile (external)
</ button >
< ControlledTabs activeTab = {tab} onTabChange = {setTab}>
< Tabs.List >
< Tabs.Tab id = "home" >Home</ Tabs.Tab >
< Tabs.Tab id = "profile" >Profile</ Tabs.Tab >
</ Tabs.List >
< Tabs.Panel id = "home" >Home</ Tabs.Panel >
< Tabs.Panel id = "profile" >Profile</ Tabs.Panel >
</ ControlledTabs >
</>
);
}
Compound Component Benefits: Flexible, declarative API; implicit state sharing; easy to extend;
semantic JSX structure; reduced prop drilling; better separation of concerns.
13.4 Custom Hooks and Logic Reuse Patterns
Hook Pattern
Purpose
Returns
Use Case
State Hook
Encapsulate stateful logic
[state, setState]
Toggle, counter, form state, modal open/close
Effect Hook
Side effects and subscriptions
void or cleanup
Event listeners, timers, subscriptions, polling
Data Hook
Data fetching and caching
{ data, loading, error }
API calls, async data, pagination, infinite scroll
Computation Hook
Derived state and calculations
computedValue
Filtering, sorting, validation, formatting
Ref Hook
DOM access, stable refs
ref
Focus management, measurements, previous values
Composite Hook
Combine multiple hooks
{ ...multiple values }
Complex workflows, coordinated state
Example: Custom hooks for common patterns
// Toggle hook
function useToggle ( initialValue = false ) {
const [ value , setValue ] = useState (initialValue);
const toggle = useCallback (() => setValue ( v => ! v), []);
const setTrue = useCallback (() => setValue ( true ), []);
const setFalse = useCallback (() => setValue ( false ), []);
return [value, toggle, setTrue, setFalse];
}
// Usage
const [ isOpen , toggle , open , close ] = useToggle ();
// Local storage hook
function useLocalStorage ( key , initialValue ) {
const [ storedValue , setStoredValue ] = useState (() => {
try {
const item = window.localStorage. getItem (key);
return item ? JSON . parse (item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback (( value ) => {
try {
const valueToStore = value instanceof Function ? value (storedValue) : value;
setStoredValue (valueToStore);
window.localStorage. setItem (key, JSON . stringify (valueToStore));
} catch (error) {
console. error ( 'Error saving to localStorage:' , error);
}
}, [key, storedValue]);
return [storedValue, setValue];
}
// Previous value hook
function usePrevious ( value ) {
const ref = useRef ();
useEffect (() => {
ref.current = value;
}, [value]);
return ref.current;
}
Example: Data fetching and debounce hooks
// Fetch hook with loading/error states
function useFetch ( url , options ) {
const [ data , setData ] = useState ( null );
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState ( null );
useEffect (() => {
let isCancelled = false ;
setLoading ( true );
fetch (url, options)
. then ( res => res. json ())
. then ( data => {
if ( ! isCancelled) {
setData (data);
setError ( null );
}
})
. catch ( err => {
if ( ! isCancelled) {
setError (err.message);
setData ( null );
}
})
. finally (() => {
if ( ! isCancelled) setLoading ( false );
});
return () => { isCancelled = true ; };
}, [url, JSON . stringify (options)]);
return { data, loading, error };
}
// Debounce hook
function useDebounce ( value , delay ) {
const [ debouncedValue , setDebouncedValue ] = useState (value);
useEffect (() => {
const handler = setTimeout (() => {
setDebouncedValue (value);
}, delay);
return () => clearTimeout (handler);
}, [value, delay]);
return debouncedValue;
}
// Usage - search with debounce
function SearchComponent () {
const [ search , setSearch ] = useState ( '' );
const debouncedSearch = useDebounce (search, 500 );
const { data , loading } = useFetch ( `/api/search?q=${ debouncedSearch }` );
return (
< div >
< input value = {search} onChange = { e => setSearch (e.target.value)} />
{loading ? < Spinner /> : < Results data = {data} />}
</ div >
);
}
Custom Hook Guidelines: Always start with 'use' prefix, follow hooks rules, return
values/functions consumers need, use useCallback for returned functions, document parameters and return values,
handle cleanup in useEffect.
13.5 Polymorphic Components and Generic Props
Pattern
Implementation
Description
Use Case
As Prop Pattern
<Box as="button">
Component renders as different HTML elements
Flexible primitive components, design systems
Component Prop
<Comp component={Link}>
Accept component as prop to customize rendering
Wrapper components, list items, custom elements
Render Function
renderItem={(item) => ...}
Function prop that returns rendered content
Lists, grids, custom rendering logic
TypeScript Generic
<T extends ElementType>
Type-safe polymorphic components with TS TS
Fully typed props based on element type
Example: Polymorphic Box component
// Basic polymorphic component
function Box ({ as : Component = 'div' , children , ... props }) {
return < Component { ... props}>{children}</ Component >;
}
// Usage - renders as different elements
< Box >Default div</ Box >
< Box as = "section" >Section element</ Box >
< Box as = "button" onClick = {handleClick}>Button element</ Box >
< Box as = {Link} to = "/home" >React Router Link</ Box >
// TypeScript polymorphic component with full type safety
import { ElementType, ComponentPropsWithoutRef } from 'react' ;
type PolymorphicProps < T extends ElementType > = {
as ?: T ;
children ?: React . ReactNode ;
} & ComponentPropsWithoutRef < T >;
function PolymorphicBox < T extends ElementType = 'div' >({
as ,
children ,
... props
} : PolymorphicProps < T >) {
const Component = as || 'div' ;
return < Component { ... props}>{children}</ Component >;
}
// TypeScript enforces correct props based on 'as' prop
< PolymorphicBox as = "a" href = "/path" >Link</ PolymorphicBox > // ✓ href allowed
< PolymorphicBox as = "button" onClick = {fn}>Button</ PolymorphicBox > // ✓ onClick allowed
< PolymorphicBox as = "div" href = "/path" >Div</ PolymorphicBox > // ✗ TS error - href not valid
Example: Generic list component
// Generic list with custom rendering
function List ({ items , renderItem , emptyMessage = 'No items' }) {
if ( ! items?. length ) {
return < div className = "empty" >{emptyMessage}</ div >;
}
return (
< ul className = "list" >
{items. map (( item , index ) => (
< li key = {item.id || index}>
{ renderItem (item, index)}
</ li >
))}
</ ul >
);
}
// Usage with different render functions
< List
items = {users}
renderItem = {( user ) => < UserCard user = {user} />}
/>
< List
items = {products}
renderItem = {( product , idx ) => (
< div >
{idx + 1 }. {product.name} - ${product.price}
</ div >
)}
/>
// With component prop pattern
function Container ({ component : Component = 'div' , children , ... props }) {
return (
< Component className = "container" { ... props}>
{children}
</ Component >
);
}
// Usage
< Container >Default div container</ Container >
< Container component = "section" >Section container</ Container >
< Container component = {Card}>Custom component</ Container >
Polymorphic Component Benefits: Single component, multiple elements; reduces component
proliferation; type-safe with TypeScript; flexible for design systems; maintains consistent styling/behavior.
13.6 Component Composition vs Inheritance
Approach
React Recommendation
Pattern
Reason
Composition PREFERRED
Strongly recommended
Children prop, props, slots
More flexible, easier to understand, React way
Inheritance AVOID
Not recommended
Class extends Component
Tight coupling, less flexible, not idiomatic React
Containment
Use children prop
<Box>{children}</Box>
Generic containers, wrappers, layouts
Specialization
Compose with props
<Dialog type="alert">
Specific variants from generic components
Named Slots
Multiple props
header={...} footer={...}
Complex layouts with multiple sections
Example: Composition patterns (preferred)
// Containment - generic container with children
function Dialog ({ title , children , footer }) {
return (
< div className = "dialog" >
< div className = "dialog-header" >
< h2 >{title}</ h2 >
</ div >
< div className = "dialog-body" >
{children}
</ div >
{footer && (
< div className = "dialog-footer" >
{footer}
</ div >
)}
</ div >
);
}
// Specialization - specific dialog variants via composition
function WelcomeDialog () {
return (
< Dialog
title = "Welcome"
footer = {
< button onClick = {handleStart}>Get Started</ button >
}
>
< p >Thank you for joining!</ p >
</ Dialog >
);
}
function ConfirmDialog ({ onConfirm , onCancel }) {
return (
< Dialog
title = "Confirm Action"
footer = {
<>
< button onClick = {onCancel}>Cancel</ button >
< button onClick = {onConfirm}>Confirm</ button >
</>
}
>
< p >Are you sure you want to proceed?</ p >
</ Dialog >
);
}
// Named slots pattern
function Layout ({ header , sidebar , main , footer }) {
return (
< div className = "layout" >
< header >{header}</ header >
< div className = "layout-body" >
< aside >{sidebar}</ aside >
< main >{main}</ main >
</ div >
< footer >{footer}</ footer >
</ div >
);
}
// Usage
< Layout
header = {< Header />}
sidebar = {< Sidebar />}
main = {< MainContent />}
footer = {< Footer />}
/>
Example: Why composition beats inheritance
// ❌ Inheritance approach (avoid)
class BaseDialog extends Component {
render () {
return (
< div className = "dialog" >
< h2 >{ this .props.title}</ h2 >
{ this . renderContent ()}
{ this . renderActions ()}
</ div >
);
}
}
class WelcomeDialog extends BaseDialog {
renderContent () {
return < p >Welcome!</ p >;
}
renderActions () {
return < button >Get Started</ button >;
}
}
// Issues: Tight coupling, hard to modify, fragile base class
// ✓ Composition approach (preferred)
function Dialog ({ title , children , actions }) {
return (
< div className = "dialog" >
< h2 >{title}</ h2 >
< div >{children}</ div >
< div >{actions}</ div >
</ div >
);
}
function WelcomeDialog () {
return (
< Dialog
title = "Welcome"
actions = {< button >Get Started</ button >}
>
< p >Welcome!</ p >
</ Dialog >
);
}
// Benefits: Flexible, clear, easy to modify, testable
// Composition with hooks for behavior reuse
function useDialogState () {
const [ isOpen , setIsOpen ] = useState ( false );
return { isOpen, open : () => setIsOpen ( true ), close : () => setIsOpen ( false ) };
}
function MyComponent () {
const dialog = useDialogState ();
return (
<>
< button onClick = {dialog.open}>Open Dialog</ button >
{dialog.isOpen && (
< Dialog title = "My Dialog" onClose = {dialog.close}>
Content here
</ Dialog >
)}
</>
);
}
Warning: React does NOT recommend using inheritance for component reuse. Props and composition
give you all the flexibility you need. Use hooks for behavior reuse, not class inheritance.
14. React Suspense and Concurrent Features REACT 18+
14.1 Suspense Component for Code Splitting
Feature
Syntax
Description
Use Case
React.lazy()
lazy(() => import('./Comp'))
Dynamic import for code splitting
Load components only when needed
Suspense
<Suspense fallback={...}>
Show fallback while lazy component loads
Loading states for async components
fallback Prop
fallback={<Spinner/>}
Component shown during loading
Skeleton screens, spinners, placeholders
Nested Suspense
<Suspense>...<Suspense>
Multiple suspense boundaries at different levels
Granular loading states, progressive loading
Named Imports
lazy(() => import().then())
Load named exports with lazy
Non-default exports from modules
Example: Basic code splitting with Suspense
import { lazy, Suspense } from 'react' ;
// Lazy load components
const Dashboard = lazy (() => import ( './Dashboard' ));
const Profile = lazy (() => import ( './Profile' ));
const Settings = lazy (() => import ( './Settings' ));
function App () {
return (
< div >
< Header /> { /* Always loaded */ }
< Suspense fallback = {< LoadingSpinner />}>
< Routes >
< Route path = "/dashboard" element = {< Dashboard />} />
< Route path = "/profile" element = {< Profile />} />
< Route path = "/settings" element = {< Settings />} />
</ Routes >
</ Suspense >
</ div >
);
}
// Named export lazy loading
const MyComponent = lazy (() =>
import ( './MyModule' ). then ( module => ({
default: module .MyNamedExport
}))
);
Example: Nested Suspense boundaries for granular loading
const MainContent = lazy (() => import ( './MainContent' ));
const Sidebar = lazy (() => import ( './Sidebar' ));
const Comments = lazy (() => import ( './Comments' ));
function Page () {
return (
< div >
{ /* Top-level suspense for entire page */ }
< Suspense fallback = {< PageSkeleton />}>
< div className = "layout" >
{ /* Separate boundary for main content */ }
< Suspense fallback = {< ContentSkeleton />}>
< MainContent />
{ /* Nested boundary for comments */ }
< Suspense fallback = {< div >Loading comments...</ div >}>
< Comments />
</ Suspense >
</ Suspense >
{ /* Separate boundary for sidebar */ }
< Suspense fallback = {< SidebarSkeleton />}>
< Sidebar />
</ Suspense >
</ div >
</ Suspense >
</ div >
);
}
// Route-based code splitting
function App () {
return (
< Router >
< Suspense fallback = {< FullPageSpinner />}>
< Routes >
< Route path = "/" element = { lazy (() => import ( './Home' ))} />
< Route path = "/about" element = { lazy (() => import ( './About' ))} />
</ Routes >
</ Suspense >
</ Router >
);
}
Code Splitting Best Practices: Split at route level first, use nested Suspense for progressive
loading, show meaningful fallbacks (skeletons better than spinners), preload critical routes, split large
third-party libraries separately.
14.2 Suspense for Data Fetching Patterns
Pattern
Implementation
Description
Status
Suspense-enabled Libraries
React Query, SWR, Relay
Libraries with built-in Suspense support
Stable
use() Hook REACT 19
const data = use(promise)
Read promise value, suspends until resolved
React 19+
Resource Pattern
Wrap promise in resource object
Manual Suspense implementation pattern
Advanced
Server Components
Async components on server
Native async/await in components
Next.js 13+
Streaming SSR
renderToPipeableStream
Stream HTML with Suspense boundaries
React 18+
Example: Suspense with React Query
import { useQuery } from '@tanstack/react-query' ;
import { Suspense } from 'react' ;
// Component that uses Suspense for data fetching
function UserProfile ({ userId }) {
const { data : user } = useQuery ({
queryKey: [ 'user' , userId],
queryFn : () => fetchUser (userId),
suspense: true // Enable Suspense mode
});
return (
< div >
< h1 >{user.name}</ h1 >
< p >{user.email}</ p >
</ div >
);
}
// Wrap with Suspense
function App () {
return (
< Suspense fallback = {< UserSkeleton />}>
< UserProfile userId = { 123 } />
</ Suspense >
);
}
// Multiple parallel data fetches with Suspense
function Dashboard () {
return (
< Suspense fallback = {< DashboardSkeleton />}>
< div className = "dashboard" >
< Suspense fallback = {< div >Loading stats...</ div >}>
< Stats />
</ Suspense >
< Suspense fallback = {< div >Loading activity...</ div >}>
< RecentActivity />
</ Suspense >
< Suspense fallback = {< div >Loading chart...</ div >}>
< Chart />
</ Suspense >
</ div >
</ Suspense >
);
}
Example: use() hook for Suspense data fetching (React 19)
import { use, Suspense } from 'react' ;
// Fetch function returns a promise
async function fetchUser ( id ) {
const response = await fetch ( `/api/users/${ id }` );
return response. json ();
}
// Component using use() hook
function UserProfile ({ userPromise }) {
// use() suspends component until promise resolves
const user = use (userPromise);
return (
< div >
< h1 >{user.name}</ h1 >
< p >Email: {user.email}</ p >
</ div >
);
}
// Parent component
function App () {
// Start fetch immediately (not in component)
const userPromise = fetchUser ( 123 );
return (
< Suspense fallback = {< LoadingSkeleton />}>
< UserProfile userPromise = {userPromise} />
</ Suspense >
);
}
// Conditional data fetching with use()
function ConditionalData ({ showData }) {
if ( ! showData) {
return < div >Enable data display</ div >;
}
// use() can be called conditionally (unlike hooks!)
const data = use ( fetchData ());
return < div >{data.content}</ div >;
}
Warning: Suspense for data fetching requires libraries with Suspense integration (React Query,
SWR, Relay) or React 19's use() hook. Don't throw promises manually unless implementing a proper resource
pattern.
14.3 Error Boundaries with Suspense
Concept
Implementation
Description
Use Case
Error Boundary Wrapper
Wrap Suspense with ErrorBoundary
Catch errors from suspended components
Handle loading failures gracefully
Separate Boundaries
ErrorBoundary per Suspense
Independent error handling for sections
Prevent whole page failure
Retry Logic
Reset error boundary state
Allow user to retry failed loads
Temporary network issues, transient errors
Error Fallback UI
Custom error components
User-friendly error messages
Better UX than blank screens
Example: Error Boundary with Suspense integration
import { ErrorBoundary } from 'react-error-boundary' ;
import { Suspense } from 'react' ;
// Error fallback component with retry
function ErrorFallback ({ error , resetErrorBoundary }) {
return (
< div role = "alert" className = "error-container" >
< h2 >Something went wrong</ h2 >
< pre >{error.message}</ pre >
< button onClick = {resetErrorBoundary}>
Try again
</ button >
</ div >
);
}
// Combined Error + Suspense boundary
function Page () {
return (
< ErrorBoundary
FallbackComponent = {ErrorFallback}
onReset = {() => {
// Reset any cached data
queryClient. resetQueries ();
}}
>
< Suspense fallback = {< LoadingSkeleton />}>
< UserProfile />
</ Suspense >
</ ErrorBoundary >
);
}
// Multiple boundaries for granular error handling
function Dashboard () {
return (
< div className = "dashboard" >
{ /* Each section has its own error boundary */ }
< ErrorBoundary fallback = {< SectionError section = "Stats" />}>
< Suspense fallback = {< StatsSkeleton />}>
< Stats />
</ Suspense >
</ ErrorBoundary >
< ErrorBoundary fallback = {< SectionError section = "Activity" />}>
< Suspense fallback = {< ActivitySkeleton />}>
< RecentActivity />
</ Suspense >
</ ErrorBoundary >
< ErrorBoundary fallback = {< SectionError section = "Chart" />}>
< Suspense fallback = {< ChartSkeleton />}>
< Chart />
</ Suspense >
</ ErrorBoundary >
</ div >
);
}
Example: Reusable boundary component
// Combined ErrorBoundary + Suspense component
function AsyncBoundary ({
children ,
fallback ,
errorFallback ,
onReset
}) {
return (
< ErrorBoundary
fallbackRender = {({ error , resetErrorBoundary }) =>
errorFallback ?
errorFallback (error, resetErrorBoundary) :
< DefaultErrorFallback error = {error} reset = {resetErrorBoundary} />
}
onReset = {onReset}
>
< Suspense fallback = {fallback || < DefaultLoadingFallback />}>
{children}
</ Suspense >
</ ErrorBoundary >
);
}
// Usage - clean and simple
function App () {
return (
< AsyncBoundary
fallback = {< PageSkeleton />}
errorFallback = {( error , reset ) => (
< PageError error = {error} onRetry = {reset} />
)}
>
< UserDashboard />
</ AsyncBoundary >
);
}
Error Boundary + Suspense Pattern: Always wrap Suspense with ErrorBoundary, use granular
boundaries to prevent cascade failures, implement retry logic, show helpful error messages, log errors to
monitoring services.
14.4 Concurrent Rendering and Time Slicing
Feature
Description
Benefit
Availability
Concurrent Mode
React can interrupt, pause, resume rendering
Keeps app responsive during expensive renders
React 18+
Time Slicing
Break rendering work into chunks
Yield to browser between chunks for interactions
Automatic with React 18
Automatic Batching
Batch multiple state updates together
Fewer re-renders, better performance
Enabled by default React 18
Interruptible Rendering
React can pause low-priority work
High-priority updates don't get blocked
With concurrent features
Priority Levels
Urgent vs non-urgent updates
Keep UI responsive for user input
useTransition, useDeferredValue
Example: Concurrent rendering with createRoot
import { createRoot } from 'react-dom/client' ;
// React 18+ concurrent mode (default with createRoot)
const root = createRoot (document. getElementById ( 'root' ));
root. render (< App />);
// Automatic batching in React 18
function App () {
const [ count , setCount ] = useState ( 0 );
const [ flag , setFlag ] = useState ( false );
function handleClick () {
// React 18 batches these together automatically
// Even in async callbacks, timeouts, native events!
setCount ( c => c + 1 );
setFlag ( f => ! f);
// Only 1 re-render instead of 2
}
return (
< div >
< button onClick = {handleClick}>Next</ button >
< h1 style = {{ color: flag ? 'blue' : 'black' }}>
{count}
</ h1 >
</ div >
);
}
// Opt-out of batching if needed (rare)
import { flushSync } from 'react-dom' ;
function handleClick () {
flushSync (() => {
setCount ( c => c + 1 );
});
// DOM updated immediately
flushSync (() => {
setFlag ( f => ! f);
});
// DOM updated again - 2 separate renders
}
Example: Visualizing concurrent rendering benefits
// Without concurrent features (React 17)
// Long render blocks everything
function HeavyList ({ items }) {
// This blocks the entire UI during render
return (
< ul >
{items. map ( item => (
< ExpensiveItem key = {item.id} item = {item} />
))}
</ ul >
);
}
// User can't type or interact until render completes
// With concurrent features (React 18+)
function ResponsiveList ({ items }) {
const [ query , setQuery ] = useState ( '' );
const [ isPending , startTransition ] = useTransition ();
const [ displayItems , setDisplayItems ] = useState (items);
function handleSearch ( e ) {
const value = e.target.value;
setQuery (value); // Urgent - updates immediately
// Non-urgent - can be interrupted
startTransition (() => {
const filtered = items. filter ( item =>
item.name. includes (value)
);
setDisplayItems (filtered);
});
}
return (
< div >
< input value = {query} onChange = {handleSearch} />
{isPending && < Spinner />}
< ul className = {isPending ? 'loading' : '' }>
{displayItems. map ( item => (
< ExpensiveItem key = {item.id} item = {item} />
))}
</ ul >
</ div >
);
}
// Input stays responsive - filtering happens in background
Concurrent Features: Enabled by default with createRoot in React 18, automatic batching
improves performance automatically, time slicing keeps UI responsive, use transitions for non-urgent updates, no
breaking changes for existing code.
14.5 useTransition for State Transitions
Feature
Syntax
Description
Use Case
useTransition Hook
const [isPending, start] = useTransition()
Mark state updates as non-urgent transitions
Keep UI responsive during expensive updates
isPending Flag
isPending
Boolean indicating if transition is in progress
Show loading indicators, disable UI temporarily
startTransition
startTransition(() => {...})
Function to wrap non-urgent state updates
Large list filters, tab switches, searches
Priority
Lower than user input
Can be interrupted by urgent updates
Heavy renders don't block interactions
Example: useTransition for responsive search
import { useState, useTransition } from 'react' ;
function SearchResults () {
const [ query , setQuery ] = useState ( '' );
const [ results , setResults ] = useState ([]);
const [ isPending , startTransition ] = useTransition ();
function handleSearch ( e ) {
const value = e.target.value;
// Urgent: Update input immediately
setQuery (value);
// Non-urgent: Update results (can be interrupted)
startTransition (() => {
// Expensive filtering
const filtered = allItems. filter ( item =>
item.title. toLowerCase (). includes (value. toLowerCase ()) ||
item.description. toLowerCase (). includes (value. toLowerCase ())
);
setResults (filtered);
});
}
return (
< div >
< input
type = "text"
value = {query}
onChange = {handleSearch}
placeholder = "Search..."
/>
{isPending && < LoadingSpinner />}
< ul className = {isPending ? 'opacity-50' : '' }>
{results. map ( item => (
< SearchResultItem key = {item.id} item = {item} />
))}
</ ul >
</ div >
);
}
Example: Tab switching with transitions
function Tabs () {
const [ activeTab , setActiveTab ] = useState ( 'home' );
const [ isPending , startTransition ] = useTransition ();
function handleTabClick ( tab ) {
// Mark tab content update as transition
startTransition (() => {
setActiveTab (tab);
});
}
return (
< div >
< div className = "tab-buttons" >
< button
onClick = {() => handleTabClick ( 'home' )}
className = {activeTab === 'home' ? 'active' : '' }
>
Home
</ button >
< button
onClick = {() => handleTabClick ( 'posts' )}
className = {activeTab === 'posts' ? 'active' : '' }
>
Posts {isPending && '...' }
</ button >
< button
onClick = {() => handleTabClick ( 'contact' )}
className = {activeTab === 'contact' ? 'active' : '' }
>
Contact
</ button >
</ div >
< div className = { `tab-content ${ isPending ? 'loading' : ''}` }>
{activeTab === 'home' && < HomeTab />}
{activeTab === 'posts' && < PostsTab />} { /* Expensive */ }
{activeTab === 'contact' && < ContactTab />}
</ div >
</ div >
);
}
// Comparison: without useTransition
function SlowTabs () {
const [ activeTab , setActiveTab ] = useState ( 'home' );
// Tab clicks feel laggy because UI waits for expensive render
return (
< div >
< button onClick = {() => setActiveTab ( 'posts' )}>
Posts
</ button >
{ /* UI freezes until PostsTab renders */ }
{activeTab === 'posts' && < PostsTab />}
</ div >
);
}
function PaginatedList () {
const [ page , setPage ] = useState ( 1 );
const [ isPending , startTransition ] = useTransition ();
// Fetch data for current page
const { data , loading } = useFetch ( `/api/items?page=${ page }` );
function goToPage ( newPage ) {
startTransition (() => {
setPage (newPage);
});
}
return (
< div >
{ /* Show current data while transitioning */ }
< div className = {isPending ? 'transitioning' : '' }>
{loading ? (
< Spinner />
) : (
< ItemList items = {data} />
)}
</ div >
< div className = "pagination" >
< button
onClick = {() => goToPage (page - 1 )}
disabled = {page === 1 || isPending}
>
Previous
</ button >
< span >Page {page} {isPending && '(loading...)' }</ span >
< button
onClick = {() => goToPage (page + 1 )}
disabled = {isPending}
>
Next
</ button >
</ div >
</ div >
);
}
useTransition Best Practices: Use for expensive updates that aren't time-sensitive, keep user
input responsive (don't wrap input onChange), show isPending state to user, works with Suspense boundaries, test
on low-end devices to see benefits.
14.6 Server Suspense and Streaming SSR
Feature
API
Description
Use Case
Streaming SSR
renderToPipeableStream()
Stream HTML to client progressively
Faster initial page load, better TTFB
Selective Hydration
Automatic with Suspense
Hydrate critical parts first
Interactive faster, better TTI
Server Suspense
<Suspense> on server
Show fallback while server fetches data
Stream page with loading states
Server Components
Async components (Next.js)
Await data directly in components
Zero-bundle data fetching
Progressive Loading
Nested Suspense boundaries
Stream page sections independently
Show content as it's ready
Example: Node.js streaming SSR setup
import { renderToPipeableStream } from 'react-dom/server' ;
// Server-side streaming setup
app. get ( '/' , ( req , res ) => {
const { pipe , abort } = renderToPipeableStream (
< App />,
{
bootstrapScripts: [ '/main.js' ],
onShellReady () {
// Start streaming immediately
res. setHeader ( 'Content-Type' , 'text/html' );
pipe (res);
},
onShellError ( error ) {
res. status ( 500 ). send ( '<h1>Server Error</h1>' );
},
onAllReady () {
// All Suspense boundaries resolved
console. log ( 'All content ready' );
},
onError ( error ) {
console. error ( 'Stream error:' , error);
}
}
);
// Abort after timeout
setTimeout (() => abort (), 10000 );
});
// App with Suspense boundaries
function App () {
return (
< html >
< head >
< title >Streaming SSR App</ title >
</ head >
< body >
< Header /> { /* Rendered immediately */ }
{ /* Stream this section separately */ }
< Suspense fallback = {< CommentsSkeleton />}>
< Comments /> { /* Slow data fetch */ }
</ Suspense >
< Footer /> { /* Rendered immediately */ }
</ body >
</ html >
);
}
Example: Next.js Server Components with Suspense
// app/page.tsx (Next.js 13+ App Router)
import { Suspense } from 'react' ;
// Server Component - can be async!
async function UserProfile ({ userId }) {
// Fetch directly in component (server-side)
const user = await fetch ( `https://api.example.com/users/${ userId }` )
. then ( res => res. json ());
return (
< div >
< h1 >{user.name}</ h1 >
< p >{user.email}</ p >
</ div >
);
}
async function Posts () {
const posts = await fetch ( 'https://api.example.com/posts' )
. then ( res => res. json ());
return (
< ul >
{posts. map ( post => (
< li key = {post.id}>{post.title}</ li >
))}
</ ul >
);
}
// Page with streaming sections
export default function Page () {
return (
< div >
< h1 >My Page</ h1 >
{ /* Profile streams first */ }
< Suspense fallback = {< ProfileSkeleton />}>
< UserProfile userId = "123" />
</ Suspense >
{ /* Posts stream independently */ }
< Suspense fallback = {< PostsSkeleton />}>
< Posts />
</ Suspense >
</ div >
);
}
// HTML streams progressively as data arrives
Example: Selective hydration benefits
// Page with multiple sections
function App () {
return (
< div >
< Header /> { /* Hydrates first (small, fast) */ }
{ /* Heavy component wrapped in Suspense */ }
< Suspense fallback = {< ChartSkeleton />}>
< HeavyChart /> { /* Hydrates later, doesn't block */ }
</ Suspense >
{ /* User can interact with button before chart hydrates */ }
< button onClick = {handleClick}>
Click me (interactive immediately!)
</ button >
< Suspense fallback = {< CommentsSkeleton />}>
< Comments /> { /* Hydrates after chart */ }
</ Suspense >
</ div >
);
}
// Benefits of selective hydration:
// 1. Faster Time to Interactive (TTI)
// 2. User can interact with parts before full hydration
// 3. Heavy components don't block critical interactions
// 4. Automatic priority based on user interaction
Streaming SSR Benefits: Faster TTFB (Time to First Byte), progressive page rendering, selective
hydration for better TTI, better perceived performance, works with Suspense boundaries, built into Next.js 13+
App Router.
15. Portals and DOM Rendering Control
15.1 createPortal for DOM Teleportation
Feature
Syntax
Description
Use Case
createPortal
createPortal(child, container)
Render children into different DOM node
Modals, tooltips, dropdowns outside parent DOM
Target Container
document.getElementById('id')
DOM element to render portal into
Dedicated portal root, body, custom container
Event Bubbling
Bubbles through React tree
Events bubble to React parent, not DOM parent
Event handlers work as expected in React
Context
Inherits from React tree
Portal components access parent context
Theme, auth, state available in portals
Multiple Portals
Multiple createPortal calls
Render to different containers
Multiple modals, tooltips, overlays
Example: Basic portal setup
import { createPortal } from 'react-dom' ;
// HTML structure needed
// <div id="root"></div>
// <div id="portal-root"></div>
function App () {
return (
< div className = "app" >
< h1 >My App</ h1 >
< PortalExample />
</ div >
);
}
function PortalExample () {
const [ show , setShow ] = useState ( false );
return (
< div >
< button onClick = {() => setShow ( true )}>
Show Portal
</ button >
{show && (
createPortal (
< div className = "portal-content" >
< h2 >I'm rendered in #portal-root!</ h2 >
< button onClick = {() => setShow ( false )}>
Close
</ button >
</ div >,
document. getElementById ( 'portal-root' )
)
)}
</ div >
);
}
// Portal component wrapper
function Portal ({ children , container = 'portal-root' }) {
const [ mounted , setMounted ] = useState ( false );
useEffect (() => {
setMounted ( true );
return () => setMounted ( false );
}, []);
if ( ! mounted) return null ;
const portalRoot = document. getElementById (container);
if ( ! portalRoot) {
console. error ( `Portal container #${ container } not found` );
return null ;
}
return createPortal (children, portalRoot);
}
Example: Dynamic portal container
// Create portal container on demand
function usePortal ( id = 'portal-root' ) {
const [ container , setContainer ] = useState ( null );
useEffect (() => {
// Check if container exists
let element = document. getElementById (id);
// Create if doesn't exist
if ( ! element) {
element = document. createElement ( 'div' );
element.id = id;
document.body. appendChild (element);
}
setContainer (element);
// Cleanup - remove if we created it
return () => {
if (element && element.childNodes. length === 0 ) {
element. remove ();
}
};
}, [id]);
return container;
}
// Usage
function MyComponent () {
const portalContainer = usePortal ( 'my-portal' );
if ( ! portalContainer) return null ;
return createPortal (
< div >Portal content</ div >,
portalContainer
);
}
Portal Benefits: Render outside parent DOM hierarchy, avoid CSS overflow/z-index issues,
maintain React component tree structure, events bubble through React tree, access parent context and props.
15.2 Modal Components with Portals
Feature
Implementation
Description
Benefit
Overlay/Backdrop
Full-screen div with fixed position
Semi-transparent background behind modal
Focus attention, prevent background interaction
Body Scroll Lock
document.body.style.overflow
Disable scrolling when modal open
Keep user focused on modal content
Focus Trap
Manage focus within modal
Tab cycles through modal elements only
Keyboard accessibility, prevents focus escape
ESC to Close
Keyboard event listener
Close modal on Escape key
Expected behavior, better UX
Click Outside
Backdrop click handler
Close modal when clicking backdrop
Intuitive dismissal, flexible UX
Example: Full-featured modal with portal
import { createPortal } from 'react-dom' ;
import { useEffect, useRef } from 'react' ;
function Modal ({ isOpen , onClose , children , title }) {
const modalRef = useRef ( null );
// Lock body scroll when modal open
useEffect (() => {
if (isOpen) {
document.body.style.overflow = 'hidden' ;
return () => {
document.body.style.overflow = 'unset' ;
};
}
}, [isOpen]);
// Close on ESC key
useEffect (() => {
if ( ! isOpen) return ;
const handleEscape = ( e ) => {
if (e.key === 'Escape' ) onClose ();
};
document. addEventListener ( 'keydown' , handleEscape);
return () => document. removeEventListener ( 'keydown' , handleEscape);
}, [isOpen, onClose]);
// Focus trap
useEffect (() => {
if ( ! isOpen || ! modalRef.current) return ;
const focusableElements = modalRef.current. querySelectorAll (
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[ 0 ];
const lastElement = focusableElements[focusableElements. length - 1 ];
// Focus first element
firstElement?. focus ();
const handleTab = ( e ) => {
if (e.key !== 'Tab' ) return ;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e. preventDefault ();
lastElement?. focus ();
}
} else {
if (document.activeElement === lastElement) {
e. preventDefault ();
firstElement?. focus ();
}
}
};
document. addEventListener ( 'keydown' , handleTab);
return () => document. removeEventListener ( 'keydown' , handleTab);
}, [isOpen]);
if ( ! isOpen) return null ;
return createPortal (
< div className = "modal-overlay" onClick = {onClose}>
< div
ref = {modalRef}
className = "modal-content"
onClick = {( e ) => e. stopPropagation ()} // Prevent close on content click
role = "dialog"
aria-modal = "true"
aria-labelledby = "modal-title"
>
< div className = "modal-header" >
< h2 id = "modal-title" >{title}</ h2 >
< button
onClick = {onClose}
aria-label = "Close modal"
className = "modal-close"
>
×
</ button >
</ div >
< div className = "modal-body" >
{children}
</ div >
</ div >
</ div >,
document.body
);
}
// Usage
function App () {
const [ isModalOpen , setIsModalOpen ] = useState ( false );
return (
< div >
< button onClick = {() => setIsModalOpen ( true )}>
Open Modal
</ button >
< Modal
isOpen = {isModalOpen}
onClose = {() => setIsModalOpen ( false )}
title = "My Modal"
>
< p >Modal content goes here</ p >
< button onClick = {() => setIsModalOpen ( false )}>
Close
</ button >
</ Modal >
</ div >
);
}
Example: Modal with animation
import { useState, useEffect } from 'react' ;
import { createPortal } from 'react-dom' ;
function AnimatedModal ({ isOpen , onClose , children }) {
const [ shouldRender , setShouldRender ] = useState ( false );
const [ isAnimating , setIsAnimating ] = useState ( false );
useEffect (() => {
if (isOpen) {
setShouldRender ( true );
// Trigger animation after render
setTimeout (() => setIsAnimating ( true ), 10 );
} else {
setIsAnimating ( false );
// Remove from DOM after animation
setTimeout (() => setShouldRender ( false ), 300 );
}
}, [isOpen]);
if ( ! shouldRender) return null ;
return createPortal (
< div
className = { `modal-overlay ${ isAnimating ? 'open' : ''}` }
onClick = {onClose}
>
< div
className = { `modal-content ${ isAnimating ? 'open' : ''}` }
onClick = {( e ) => e. stopPropagation ()}
>
{children}
</ div >
</ div >,
document.body
);
}
/* CSS for animation
.modal-overlay {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.modal-overlay.open {
opacity: 1;
}
.modal-content {
transform: scale(0.7);
opacity: 0;
transition: all 300ms ease-in-out;
}
.modal-content.open {
transform: scale(1);
opacity: 1;
}
*/
Warning: Always lock body scroll, implement keyboard accessibility (ESC, focus trap), use
proper ARIA attributes, clean up event listeners, test with screen readers.
Technique
Implementation
Description
Use Case
getBoundingClientRect
Element position calculation
Get trigger element position and size
Position tooltip relative to trigger
Positioning Library
Popper.js, Floating UI
Smart positioning with collision detection
Complex tooltips, dropdowns, popovers
Dynamic Position
Calculate on show/scroll/resize
Update position when trigger moves
Scrollable containers, responsive layouts
Flip/Shift
Adjust if outside viewport
Prevent tooltip from being cut off
Edges of screen, small viewports
Arrow Positioning
Calculated arrow pointer position
Arrow points to trigger element
Visual connection between elements
import { useState, useRef, useEffect } from 'react' ;
import { createPortal } from 'react-dom' ;
function Tooltip ({ children , content , position = 'top' }) {
const [ isVisible , setIsVisible ] = useState ( false );
const [ coords , setCoords ] = useState ({ top: 0 , left: 0 });
const triggerRef = useRef ( null );
const tooltipRef = useRef ( null );
const calculatePosition = () => {
if ( ! triggerRef.current || ! tooltipRef.current) return ;
const triggerRect = triggerRef.current. getBoundingClientRect ();
const tooltipRect = tooltipRef.current. getBoundingClientRect ();
let top = 0 ;
let left = 0 ;
const gap = 8 ; // Space between trigger and tooltip
switch (position) {
case 'top' :
top = triggerRect.top - tooltipRect.height - gap;
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2 ;
break ;
case 'bottom' :
top = triggerRect.bottom + gap;
left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2 ;
break ;
case 'left' :
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2 ;
left = triggerRect.left - tooltipRect.width - gap;
break ;
case 'right' :
top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2 ;
left = triggerRect.right + gap;
break ;
}
// Prevent overflow
top = Math. max (gap, Math. min (top, window.innerHeight - tooltipRect.height - gap));
left = Math. max (gap, Math. min (left, window.innerWidth - tooltipRect.width - gap));
setCoords ({ top, left });
};
useEffect (() => {
if (isVisible) {
calculatePosition ();
window. addEventListener ( 'scroll' , calculatePosition);
window. addEventListener ( 'resize' , calculatePosition);
return () => {
window. removeEventListener ( 'scroll' , calculatePosition);
window. removeEventListener ( 'resize' , calculatePosition);
};
}
}, [isVisible]);
return (
<>
< span
ref = {triggerRef}
onMouseEnter = {() => setIsVisible ( true )}
onMouseLeave = {() => setIsVisible ( false )}
onFocus = {() => setIsVisible ( true )}
onBlur = {() => setIsVisible ( false )}
>
{children}
</ span >
{isVisible && createPortal (
< div
ref = {tooltipRef}
className = "tooltip"
role = "tooltip"
style = {{
position: 'fixed' ,
top: `${ coords . top }px` ,
left: `${ coords . left }px` ,
zIndex: 9999
}}
>
{content}
</ div >,
document.body
)}
</>
);
}
// Usage
< p >
Hover over < Tooltip content = "This is a tooltip!" >
< strong >this text</ strong >
</ Tooltip > to see tooltip.
</ p >
Example: Advanced positioning with Floating UI
import { useFloating, flip, shift, offset, arrow } from '@floating-ui/react' ;
import { useRef } from 'react' ;
import { createPortal } from 'react-dom' ;
function AdvancedTooltip ({ children , content }) {
const [ isOpen , setIsOpen ] = useState ( false );
const arrowRef = useRef ( null );
const { x , y , strategy , refs , middlewareData } = useFloating ({
open: isOpen,
placement: 'top' ,
middleware: [
offset ( 8 ), // Gap between trigger and tooltip
flip (), // Flip to opposite side if no space
shift ({ padding: 8 }), // Shift to stay in viewport
arrow ({ element: arrowRef }) // Position arrow
]
});
const arrowX = middlewareData.arrow?.x;
const arrowY = middlewareData.arrow?.y;
return (
<>
< span
ref = {refs.setReference}
onMouseEnter = {() => setIsOpen ( true )}
onMouseLeave = {() => setIsOpen ( false )}
>
{children}
</ span >
{isOpen && createPortal (
< div
ref = {refs.setFloating}
style = {{
position: strategy,
top: y ?? 0 ,
left: x ?? 0 ,
zIndex: 9999
}}
className = "tooltip"
>
{content}
< div
ref = {arrowRef}
style = {{
position: 'absolute' ,
left: arrowX != null ? `${ arrowX }px` : '' ,
top: arrowY != null ? `${ arrowY }px` : '' ,
transform: 'rotate(45deg)'
}}
className = "tooltip-arrow"
/>
</ div >,
document.body
)}
</>
);
}
Positioning Best Practices: Use positioning libraries for complex cases, recalculate on
scroll/resize, prevent viewport overflow, add collision detection, position arrow correctly, test on mobile
devices.
15.4 Event Bubbling and Portal Event Handling
Concept
Behavior
Description
Implication
React Tree Bubbling
Events bubble through React tree
Not DOM tree - React component hierarchy
Parent handlers work even though DOM separate
onClick Propagation
Bubbles to React parent
Portal click triggers parent onClick
Event handlers work naturally
stopPropagation
e.stopPropagation()
Stop event from bubbling in React tree
Prevent parent handlers from firing
Context Access
Portal accesses parent context
Context flows through React tree
Portal components have full context access
Native Events
Don't bubble through portals
addEventListener on DOM doesn't cross portal
Use React events, not native listeners
Example: Event bubbling demonstration
import { createPortal } from 'react-dom' ;
function EventBubblingDemo () {
const [ log , setLog ] = useState ([]);
const addLog = ( message ) => {
setLog ( prev => [ ... prev, message]);
};
return (
< div
onClick = {() => addLog ( 'Parent clicked' )}
style = {{ padding: '20px' , border: '2px solid blue' }}
>
< h3 >Parent Component</ h3 >
< button onClick = {() => addLog ( 'Regular button clicked' )}>
Regular Button (in DOM tree)
</ button >
{ createPortal (
< div
style = {{
position: 'fixed' ,
top: '100px' ,
right: '20px' ,
padding: '20px' ,
border: '2px solid red' ,
background: 'white'
}}
>
< h3 >Portal Component</ h3 >
< p >I'm rendered outside the parent DOM!</ p >
{ /* This click WILL bubble to parent in React tree */ }
< button onClick = {() => addLog ( 'Portal button clicked' )}>
Portal Button
</ button >
{ /* This prevents bubbling */ }
< button onClick = {( e ) => {
e. stopPropagation ();
addLog ( 'Portal button (no bubble) clicked' );
}}>
Portal Button (stopPropagation)
</ button >
</ div >,
document.body
)}
< div style = {{ marginTop: '20px' }}>
< h4 >Event Log:</ h4 >
< ul >
{log. map (( item , i ) => (
< li key = {i}>{item}</ li >
))}
</ ul >
</ div >
</ div >
);
}
// Result: Portal button click triggers BOTH:
// 1. "Portal button clicked"
// 2. "Parent clicked" (bubbled through React tree)
// But "Portal button (no bubble)" only triggers:
// 1. "Portal button (no bubble) clicked"
// (stopPropagation prevents parent handler)
Example: Context access in portals
import { createContext, useContext } from 'react' ;
import { createPortal } from 'react-dom' ;
const ThemeContext = createContext ( 'light' );
function App () {
const [ theme , setTheme ] = useState ( 'light' );
return (
< ThemeContext.Provider value = {theme}>
< div className = { `app ${ theme }` }>
< h1 >Main App</ h1 >
< button onClick = {() => setTheme ( t => t === 'light' ? 'dark' : 'light' )}>
Toggle Theme
</ button >
< PortalComponent />
</ div >
</ ThemeContext.Provider >
);
}
function PortalComponent () {
// Portal component can access parent context!
const theme = useContext (ThemeContext);
return createPortal (
< div className = { `portal ${ theme }` }>
< h2 >I'm in a portal</ h2 >
< p >Current theme: {theme}</ p >
< p >Context works across portals!</ p >
</ div >,
document.body
);
}
// Theme updates in parent flow to portal component
// Even though rendered in different DOM location
Event Bubbling Key Points: React events bubble through component tree not DOM tree, portal
events reach parent handlers naturally, use stopPropagation to prevent bubbling, context and props work across
portals, native DOM events don't cross portal boundary.
15.5 Accessibility Considerations with Portals
Concern
Solution
ARIA Attribute
Why Important
Screen Reader Announcement
Use proper role attributes
role="dialog"
Announce modal/dialog to screen readers
Modal State
Indicate modal is active
aria-modal="true"
Screen reader understands context
Label Association
Link title to dialog
aria-labelledby
Identify dialog purpose
Focus Management
Move focus to modal
Focus first element
Keyboard users start in modal
Focus Trap
Keep focus within modal
Tab cycling
Prevent accessing hidden content
Return Focus
Restore focus after close
Store trigger element
User returns to where they were
Hidden Content
Mark background as hidden
aria-hidden="true"
Screen readers skip background
Example: Accessible modal with full ARIA support
import { useEffect, useRef } from 'react' ;
import { createPortal } from 'react-dom' ;
function AccessibleModal ({ isOpen , onClose , title , children }) {
const modalRef = useRef ( null );
const triggerRef = useRef (document.activeElement);
const mainContentRef = useRef (document. getElementById ( 'main-content' ));
// Focus management
useEffect (() => {
if ( ! isOpen) return ;
// Store trigger element
triggerRef.current = document.activeElement;
// Hide main content from screen readers
if (mainContentRef.current) {
mainContentRef.current. setAttribute ( 'aria-hidden' , 'true' );
mainContentRef.current. setAttribute ( 'inert' , '' ); // Prevent interaction
}
// Focus first focusable element in modal
const focusable = modalRef.current?. querySelector (
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
focusable?. focus ();
return () => {
// Restore main content
if (mainContentRef.current) {
mainContentRef.current. removeAttribute ( 'aria-hidden' );
mainContentRef.current. removeAttribute ( 'inert' );
}
// Return focus to trigger
if (triggerRef.current instanceof HTMLElement ) {
triggerRef.current. focus ();
}
};
}, [isOpen]);
// ESC and focus trap
useEffect (() => {
if ( ! isOpen || ! modalRef.current) return ;
const handleKeyDown = ( e ) => {
// Close on ESC
if (e.key === 'Escape' ) {
onClose ();
return ;
}
// Focus trap
if (e.key === 'Tab' ) {
const focusableElements = modalRef.current. querySelectorAll (
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[ 0 ];
const lastElement = focusableElements[focusableElements. length - 1 ];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
e. preventDefault ();
lastElement?. focus ();
}
} else {
if (document.activeElement === lastElement) {
e. preventDefault ();
firstElement?. focus ();
}
}
}
};
document. addEventListener ( 'keydown' , handleKeyDown);
return () => document. removeEventListener ( 'keydown' , handleKeyDown);
}, [isOpen, onClose]);
if ( ! isOpen) return null ;
return createPortal (
< div
className = "modal-overlay"
onClick = {onClose}
role = "presentation"
>
< div
ref = {modalRef}
className = "modal-content"
onClick = {( e ) => e. stopPropagation ()}
role = "dialog"
aria-modal = "true"
aria-labelledby = "modal-title"
aria-describedby = "modal-description"
>
< div className = "modal-header" >
< h2 id = "modal-title" >{title}</ h2 >
< button
onClick = {onClose}
aria-label = "Close dialog"
className = "modal-close"
>
< span aria-hidden = "true" >×</ span >
</ button >
</ div >
< div id = "modal-description" className = "modal-body" >
{children}
</ div >
</ div >
</ div >,
document.body
);
}
function AccessibleTooltip ({ children , content , id }) {
const [ isVisible , setIsVisible ] = useState ( false );
const tooltipId = id || `tooltip-${ Math . random (). toString ( 36 ). substr ( 2 , 9 ) }` ;
return (
<>
< span
onMouseEnter = {() => setIsVisible ( true )}
onMouseLeave = {() => setIsVisible ( false )}
onFocus = {() => setIsVisible ( true )}
onBlur = {() => setIsVisible ( false )}
aria-describedby = {isVisible ? tooltipId : undefined }
tabIndex = { 0 } // Make keyboard accessible
>
{children}
</ span >
{isVisible && createPortal (
< div
id = {tooltipId}
role = "tooltip"
className = "tooltip"
style = {{
position: 'fixed' ,
// ... positioning logic
}}
>
{content}
</ div >,
document.body
)}
</>
);
}
// ARIA attributes explained:
// - aria-describedby: Links tooltip to trigger
// - role="tooltip": Identifies as tooltip
// - tabIndex={0}: Makes keyboard accessible
Accessibility Requirements: Always manage focus properly, implement keyboard navigation (ESC,
Tab), use proper ARIA attributes, hide background content, return focus on close, test with screen readers
(NVDA, JAWS, VoiceOver), support keyboard-only navigation.
15.6 Multiple Portal Management Patterns
Pattern
Implementation
Description
Use Case
Portal Stack
Array of active portals
Track z-index order of portals
Multiple modals, layered overlays
Portal Manager
Context-based management
Centralized portal state and control
Complex apps with many portals
Modal Queue
Queue system for modals
Show modals sequentially
Onboarding flows, tutorials
Z-Index Management
Auto-increment z-index
Ensure correct stacking order
Prevent overlap issues
Cleanup on Unmount
Remove portal containers
Clean up DOM when done
Prevent memory leaks, clean DOM
Example: Portal manager with context
import { createContext, useContext, useState } from 'react' ;
import { createPortal } from 'react-dom' ;
// Portal manager context
const PortalContext = createContext ( null );
export function PortalProvider ({ children }) {
const [ portals , setPortals ] = useState ([]);
const addPortal = ( portal ) => {
const id = Math. random (). toString ( 36 ). substr ( 2 , 9 );
setPortals ( prev => [ ... prev, { ... portal, id, zIndex: 1000 + prev. length }]);
return id;
};
const removePortal = ( id ) => {
setPortals ( prev => prev. filter ( p => p.id !== id));
};
const closeTopPortal = () => {
setPortals ( prev => prev. slice ( 0 , - 1 ));
};
return (
< PortalContext.Provider value = {{ portals, addPortal, removePortal, closeTopPortal }}>
{children}
{portals. map ( portal =>
createPortal (
< div key = {portal.id} style = {{ zIndex: portal.zIndex }}>
{portal.content}
</ div >,
document.body
)
)}
</ PortalContext.Provider >
);
}
// Hook to use portal manager
export function usePortal () {
const context = useContext (PortalContext);
if ( ! context) {
throw new Error ( 'usePortal must be used within PortalProvider' );
}
return context;
}
// Modal component using portal manager
function ManagedModal ({ isOpen , onClose , children , title }) {
const { addPortal , removePortal } = usePortal ();
const portalIdRef = useRef ( null );
useEffect (() => {
if (isOpen) {
portalIdRef.current = addPortal ({
content : (
< div className = "modal-overlay" onClick = {onClose}>
< div className = "modal-content" onClick = {( e ) => e. stopPropagation ()}>
< h2 >{title}</ h2 >
{children}
</ div >
</ div >
)
});
} else if (portalIdRef.current) {
removePortal (portalIdRef.current);
portalIdRef.current = null ;
}
return () => {
if (portalIdRef.current) {
removePortal (portalIdRef.current);
}
};
}, [isOpen, title, children]);
return null ;
}
// Usage
function App () {
return (
< PortalProvider >
< MyApp />
</ PortalProvider >
);
}
Example: Modal queue system
function useModalQueue () {
const [ queue , setQueue ] = useState ([]);
const [ currentModal , setCurrentModal ] = useState ( null );
const addToQueue = ( modal ) => {
setQueue ( prev => [ ... prev, modal]);
};
const showNext = () => {
if (queue. length > 0 ) {
setCurrentModal (queue[ 0 ]);
setQueue ( prev => prev. slice ( 1 ));
} else {
setCurrentModal ( null );
}
};
const closeModal = () => {
setCurrentModal ( null );
// Show next in queue after delay
setTimeout (showNext, 300 );
};
// Auto-show first modal
useEffect (() => {
if ( ! currentModal && queue. length > 0 ) {
showNext ();
}
}, [queue, currentModal]);
return { currentModal, addToQueue, closeModal, queueLength: queue. length };
}
// Usage - onboarding flow
function OnboardingFlow () {
const { currentModal , addToQueue , closeModal } = useModalQueue ();
useEffect (() => {
// Queue multiple modals
addToQueue ({ title: 'Welcome' , content: 'Step 1...' });
addToQueue ({ title: 'Features' , content: 'Step 2...' });
addToQueue ({ title: 'Get Started' , content: 'Step 3...' });
}, []);
return (
<>
{currentModal && (
< Modal
isOpen = { true }
onClose = {closeModal}
title = {currentModal.title}
>
{currentModal.content}
</ Modal >
)}
</>
);
}
Multiple Portal Best Practices: Use portal manager for complex apps, implement z-index
management, handle ESC key to close top portal, clean up portals on unmount, consider modal queue for flows,
test stacking behavior thoroughly.
16. Server Components and Full-Stack React NEXT.JS 13+
16.1 React Server Components Fundamentals
Concept
Description
Characteristic
Use Case
Server Components
Components that run only on server
No JavaScript sent to client, async by default
Data fetching, database queries, backend logic
Client Components
Traditional React components
Run on both server (SSR) and client, need 'use client'
Interactivity, hooks, browser APIs, event handlers
Zero Bundle Impact
Server component code not in bundle
Smaller JavaScript payload
Large dependencies, utilities, formatters
Direct Backend Access
Access databases, file system directly
No API route needed
Database queries, file reads, server-only logic
Async Components
Can use async/await in component
Natural data fetching pattern
Fetch data directly in component body
Streaming
Stream HTML progressively
Works with Suspense boundaries
Faster initial page load, progressive rendering
Example: Server Component basics
// app/page.tsx - Server Component by default (Next.js 13+)
// No 'use client' directive = Server Component
import { db } from '@/lib/database' ;
// Server components can be async!
async function ProductList () {
// Direct database access - no API route needed
const products = await db.product. findMany ({
include: { category: true }
});
return (
< div >
< h1 >Products</ h1 >
< ul >
{products. map ( product => (
< li key = {product.id}>
{product.name} - ${product.price}
< span >{product.category.name}</ span >
</ li >
))}
</ ul >
</ div >
);
}
export default ProductList;
// Server Component characteristics:
// ✓ Can use async/await
// ✓ Direct database/filesystem access
// ✓ No JavaScript sent to client
// ✓ Can import server-only code
// ✓ Runs only on server
// ✗ Cannot use hooks (useState, useEffect, etc.)
// ✗ Cannot use browser APIs
// ✗ Cannot have event handlers
Example: Client Component with 'use client'
// components/Counter.tsx - Client Component
'use client' ; // This directive marks it as a Client Component
import { useState } from 'react' ;
export function Counter () {
// Can use hooks - runs on client
const [ count , setCount ] = useState ( 0 );
return (
< div >
< p >Count: {count}</ p >
{ /* Event handlers work */ }
< button onClick = {() => setCount (count + 1 )}>
Increment
</ button >
</ div >
);
}
// Client Component characteristics:
// ✓ Can use hooks (useState, useEffect, etc.)
// ✓ Can use browser APIs
// ✓ Can have event handlers
// ✓ Interactive features
// ✗ Cannot be async
// ✗ JavaScript sent to client
// ✗ Cannot directly access server resources
Server vs Client Components: Server Components are the default in Next.js App Router, use 'use
client' for interactivity, server components have zero bundle impact, can mix both in same app, server
components can import client components (but not vice versa).
16.2 Client vs Server Component Boundaries
Rule
Allowed
Not Allowed
Why
Importing
Server can import Client
Client cannot import Server
Server code shouldn't run on client
Props
Pass serializable props to Client
Pass functions, class instances
Props must cross network boundary
Children Pattern
Pass Server as children to Client
Import Server into Client
Composition pattern preserves boundaries
Context
Client Components can provide context
Server Components cannot use context
Context requires client-side state
Third-party
Mark packages as 'use client' if needed
Import packages without checking
Many packages assume client environment
Example: Component composition patterns
// ✓ CORRECT: Server Component imports Client Component
// app/page.tsx (Server Component)
import { Counter } from '@/components/Counter' ; // Client Component
export default function Page () {
return (
< div >
< h1 >My Page</ h1 >
< Counter /> { /* Client Component works here */ }
</ div >
);
}
// ✗ WRONG: Client Component cannot import Server Component
// components/ClientComponent.tsx
'use client' ;
import { ServerData } from './ServerComponent' ; // ERROR!
// ✓ CORRECT: Use composition with children prop
// components/ClientWrapper.tsx
'use client' ;
export function ClientWrapper ({ children }) {
const [ state , setState ] = useState ( false );
return (
< div onClick = {() => setState ( ! state)}>
{children} { /* Server Component passed as children */ }
</ div >
);
}
// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper' ;
async function ServerData () {
const data = await fetchFromDatabase ();
return < div >{data}</ div >;
}
export default function Page () {
return (
< ClientWrapper >
< ServerData /> { /* Server Component passed to Client via children */ }
</ ClientWrapper >
);
}
Example: Serializable props requirement
// ✓ CORRECT: Serializable props
// app/page.tsx (Server Component)
< ClientComponent
name = "John"
age = { 30 }
items = {[ 'a' , 'b' , 'c' ]}
config = {{ theme: 'dark' , locale: 'en' }}
/>
// ✗ WRONG: Non-serializable props
< ClientComponent
onClick = {() => console. log ( 'click' )} // Functions can't be serialized
date = { new Date ()} // Dates lose type information
callback = {myFunction} // Functions not allowed
/>
// ✓ CORRECT: Define handlers in Client Component
'use client' ;
export function ClientComponent ({ name , age }) {
// Define event handlers in client component
const handleClick = () => {
console. log ( `${ name } clicked, age: ${ age }` );
};
return < button onClick = {handleClick}>{name}</ button >;
}
// ✓ CORRECT: Server Actions for server-side logic
// app/actions.ts
'use server' ;
export async function updateUser ( formData ) {
// Server-side logic
const name = formData. get ( 'name' );
await db.user. update ({ name });
}
// Client Component can call Server Action
'use client' ;
import { updateUser } from './actions' ;
export function Form () {
return (
< form action = {updateUser}>
< input name = "name" />
< button type = "submit" >Save</ button >
</ form >
);
}
Warning: Server Components cannot import Client Components as modules - use children prop
pattern. Props must be serializable (no functions, class instances). Third-party packages may need 'use client'
wrapper. Test boundaries carefully.
16.3 Data Fetching in Server Components
Pattern
Implementation
Benefit
Use Case
Async Components
async function Component()
Natural async/await syntax
Database queries, API calls, file reads
Parallel Fetching
Promise.all([fetch1, fetch2])
Multiple requests simultaneously
Independent data sources
Sequential Fetching
Await each fetch separately
Dependent requests
Second request needs first's result
Streaming with Suspense
Wrap slow components in Suspense
Progressive page rendering
Show fast content first, stream slow data
Request Memoization
React dedupes identical requests
Automatic caching during render
Multiple components need same data
Server-only Code
import 'server-only'
Prevent client-side import
Database clients, secret keys, server utilities
Example: Data fetching patterns
// Async Server Component
async function UserProfile ({ userId }) {
// Direct database access
const user = await db.user. findUnique ({
where: { id: userId },
include: { posts: true , followers: true }
});
return (
< div >
< h1 >{user.name}</ h1 >
< p >{user.email}</ p >
< p >Posts: {user.posts. length }</ p >
< p >Followers: {user.followers. length }</ p >
</ div >
);
}
// Parallel data fetching
async function Dashboard () {
// Fetch multiple resources in parallel
const [ user , posts , stats ] = await Promise . all ([
db.user. findFirst (),
db.post. findMany ({ take: 10 }),
db.analytics. getStats ()
]);
return (
< div >
< UserCard user = {user} />
< PostList posts = {posts} />
< StatsWidget stats = {stats} />
</ div >
);
}
// Sequential data fetching (when dependent)
async function PostWithComments ({ postId }) {
// First, get the post
const post = await db.post. findUnique ({
where: { id: postId }
});
// Then, get comments (needs post.id)
const comments = await db.comment. findMany ({
where: { postId: post.id }
});
return (
< article >
< h1 >{post.title}</ h1 >
< p >{post.content}</ p >
< CommentList comments = {comments} />
</ article >
);
}
Example: Streaming with Suspense
// app/page.tsx
import { Suspense } from 'react' ;
// Fast component
async function QuickData () {
const data = await fetch ( 'https://api.example.com/quick' );
return < div >Quick: {data}</ div >;
}
// Slow component
async function SlowData () {
const data = await fetch ( 'https://api.example.com/slow' );
return < div >Slow: {data}</ div >;
}
export default function Page () {
return (
< div >
< h1 >Dashboard</ h1 >
{ /* Quick data renders immediately */ }
< Suspense fallback = {< div >Loading quick data...</ div >}>
< QuickData />
</ Suspense >
{ /* Slow data streams in later */ }
< Suspense fallback = {< div >Loading slow data...</ div >}>
< SlowData />
</ Suspense >
</ div >
);
}
// Result: Page streams progressively
// 1. Shell with "Loading..." fallbacks sent immediately
// 2. QuickData arrives and replaces first fallback
// 3. SlowData arrives later and replaces second fallback
Example: Server-only code protection
// lib/database.ts
import 'server-only' ; // Prevents client-side import
import { PrismaClient } from '@prisma/client' ;
// This will only run on server
export const db = new PrismaClient ();
// Server-only utilities
export async function getSecretData () {
// Safe to use environment variables
const apiKey = process.env. SECRET_API_KEY ;
return fetchWithKey (apiKey);
}
// If you try to import this in a Client Component:
// 'use client';
// import { db } from '@/lib/database'; // ERROR at build time!
// lib/utils.ts - Shared utilities
// No 'server-only' import = can be used anywhere
export function formatDate ( date ) {
return date. toLocaleDateString ();
}
// Use 'server-only' package for:
// - Database clients
// - API clients with secrets
// - Server-only utilities
// - Code that must never be exposed to client
Data Fetching Best Practices: Fetch data at component level (not page level), use Suspense for
streaming, parallelize independent requests, use request memoization (React dedupes), protect server-only code
with 'server-only' package, cache appropriately with Next.js caching strategies.
Benefit
Impact
Measurement
Example
Reduced Bundle Size
Server code not sent to client
Smaller JavaScript payloads
100KB library → 0KB on client
Faster Initial Load
Less JavaScript to parse/execute
Better Time to Interactive (TTI)
Heavy formatters, utilities stay on server
Improved FCP
HTML streams immediately
First Contentful Paint
Static content shows instantly
Better SEO
Fully rendered HTML for crawlers
Complete content in initial HTML
No waiting for client-side fetch
Reduced Waterfalls
Fetch data on server in parallel
Fewer round trips
No client → server → database chain
Direct Backend Access
No API routes needed
Fewer HTTP requests
Direct database queries
Example: Bundle size comparison
// ❌ Client Component approach (old way)
'use client' ;
import moment from 'moment' ; // 67KB gzipped!
import { remark } from 'remark' ; // 50KB gzipped!
export function BlogPost ({ content , date }) {
const formattedDate = moment (date). format ( 'MMMM Do, YYYY' );
const html = remark (). processSync (content);
return (
< article >
< time >{formattedDate}</ time >
< div dangerouslySetInnerHTML = {{ __html: html }} />
</ article >
);
}
// Total bundle increase: ~117KB just for this component!
// ✅ Server Component approach (new way)
// No 'use client' = Server Component
import moment from 'moment' ; // Runs on server only
import { remark } from 'remark' ; // Runs on server only
export async function BlogPost ({ postId }) {
const post = await db.post. findUnique ({ where: { id: postId } });
const formattedDate = moment (post.date). format ( 'MMMM Do, YYYY' );
const html = remark (). processSync (post.content);
return (
< article >
< time >{formattedDate}</ time >
< div dangerouslySetInnerHTML = {{ __html: html }} />
</ article >
);
}
// Client bundle increase: 0KB! All processing on server.
// Optimize with strategic Client/Server split
// Server Component - heavy processing
async function ProductGrid () {
const products = await db.product. findMany ({
include: { reviews: true }
});
// Heavy computation on server
const productsWithRatings = products. map ( product => ({
... product,
averageRating: calculateAverage (product.reviews),
reviewCount: product.reviews. length
}));
return (
< div className = "grid" >
{productsWithRatings. map ( product => (
< ProductCard key = {product.id} product = {product} />
))}
</ div >
);
}
// Client Component - only interactive parts
'use client' ;
export function ProductCard ({ product }) {
const [ isFavorite , setIsFavorite ] = useState ( false );
return (
< div className = "card" >
{ /* Static content from server */ }
< h3 >{product.name}</ h3 >
< p >${product.price}</ p >
< div >⭐ {product.averageRating} ({product.reviewCount})</ div >
{ /* Interactive part on client */ }
< button onClick = {() => setIsFavorite ( ! isFavorite)}>
{isFavorite ? '❤️' : '🤍' } Favorite
</ button >
</ div >
);
}
// Benefits:
// ✓ Heavy computation (calculateAverage) runs on server
// ✓ Database query stays on server
// ✓ Only small interactive component sent to client
// ✓ Better performance overall
Performance Impact: Server Components significantly reduce bundle size, improve initial load
times, enable better caching strategies, reduce network waterfalls, improve Core Web Vitals (LCP, FCP, TTI),
benefit SEO with complete HTML.
16.5 Hydration and Client-side State Management
Concept
Description
Consideration
Solution
Selective Hydration
Hydrate client components only
Server component HTML stays static
Faster hydration, less JavaScript execution
State Management
Client components manage state
Server components are stateless
Use Context, Zustand, Redux in client components
Server → Client Data
Pass data via props
Props must be serializable
JSON-serializable data only
Client State Hydration
Initialize client state from server data
Avoid hydration mismatches
Use useEffect for client-only state
Progressive Enhancement
Works without JavaScript first
Server Components always work
Add interactivity with Client Components
Example: State management patterns
// Server Component fetches data
async function UserDashboard ({ userId }) {
const user = await db.user. findUnique ({
where: { id: userId },
include: { preferences: true }
});
// Pass data to Client Component
return (
< div >
< h1 >Welcome, {user.name}</ h1 >
< UserSettings initialSettings = {user.preferences} />
</ div >
);
}
// Client Component manages state
'use client' ;
export function UserSettings ({ initialSettings }) {
// Initialize state from server data
const [ settings , setSettings ] = useState (initialSettings);
const [ isSaving , setIsSaving ] = useState ( false );
const handleSave = async () => {
setIsSaving ( true );
await fetch ( '/api/settings' , {
method: 'POST' ,
body: JSON . stringify (settings)
});
setIsSaving ( false );
};
return (
< div >
< label >
Theme:
< select
value = {settings.theme}
onChange = {( e ) => setSettings ({ ... settings, theme: e.target.value})}
>
< option value = "light" >Light</ option >
< option value = "dark" >Dark</ option >
</ select >
</ label >
< button onClick = {handleSave} disabled = {isSaving}>
{isSaving ? 'Saving...' : 'Save Settings' }
</ button >
</ div >
);
}
Example: Context providers in Client Components
// app/layout.tsx - Server Component
import { Providers } from './providers' ;
export default function RootLayout ({ children }) {
return (
< html >
< body >
{ /* Providers must be Client Component */ }
< Providers >
{children}
</ Providers >
</ body >
</ html >
);
}
// app/providers.tsx - Client Component
'use client' ;
import { createContext, useContext, useState } from 'react' ;
const ThemeContext = createContext ();
export function Providers ({ children }) {
const [ theme , setTheme ] = useState ( 'light' );
return (
< ThemeContext.Provider value = {{ theme, setTheme }}>
{children}
</ ThemeContext.Provider >
);
}
export function useTheme () {
return useContext (ThemeContext);
}
// Any Client Component can now use theme
'use client' ;
import { useTheme } from './providers' ;
export function ThemeToggle () {
const { theme , setTheme } = useTheme ();
return (
< button onClick = {() => setTheme (theme === 'light' ? 'dark' : 'light' )}>
Current: {theme}
</ button >
);
}
Example: Avoiding hydration mismatches
// ❌ WRONG: Causes hydration mismatch
'use client' ;
export function TimeDisplay () {
// Server renders one time, client hydrates with different time
return < div >Current time: { new Date (). toLocaleTimeString ()}</ div >;
// ERROR: Server HTML doesn't match client hydration
}
// ✅ CORRECT: Client-only rendering
'use client' ;
export function TimeDisplay () {
const [ time , setTime ] = useState ( null );
useEffect (() => {
// Only runs on client
setTime ( new Date (). toLocaleTimeString ());
const interval = setInterval (() => {
setTime ( new Date (). toLocaleTimeString ());
}, 1000 );
return () => clearInterval (interval);
}, []);
// Show nothing during SSR, then show time on client
if ( ! time) return < div >Loading time...</ div >;
return < div >Current time: {time}</ div >;
}
// ✅ ALTERNATIVE: Suppress hydration warning (use sparingly)
'use client' ;
export function TimeDisplay () {
return (
< div suppressHydrationWarning >
Current time: { new Date (). toLocaleTimeString ()}
</ div >
);
}
Hydration Warning: Client and server HTML must match during hydration. Use useEffect for
client-only code, avoid Date.now() or random values in render, use suppressHydrationWarning sparingly, test
hydration carefully.
16.6 Next.js App Router and Server Components
Feature
File Convention
Purpose
Component Type
Page
app/page.tsx
Route page component
Server by default
Layout
app/layout.tsx
Shared layout wrapper
Server by default
Loading
app/loading.tsx
Loading UI (Suspense fallback)
Server by default
Error
app/error.tsx
Error boundary UI
Must be Client ('use client')
Template
app/template.tsx
Re-rendered on navigation
Server by default
Route Handler
app/api/route.ts
API endpoint
Server-only
Example: Next.js App Router file structure
app /
├── layout.tsx # Root layout (Server Component)
├── page.tsx # Home page (Server Component)
├── loading.tsx # Loading UI (Server Component)
├── error.tsx # Error boundary (Client Component)
├── providers.tsx # Context providers (Client Component)
│
├── dashboard /
│ ├── layout.tsx # Dashboard layout
│ ├── page.tsx # Dashboard page
│ └── loading.tsx # Dashboard loading
│
├── blog /
│ ├── page.tsx # Blog list
│ └── [slug] /
│ ├── page.tsx # Blog post (dynamic route)
│ └── loading.tsx
│
└── api /
└── users /
└── route.ts # API route handler
// app/layout.tsx - Root Layout
export default function RootLayout ({ children }) {
return (
< html lang = "en" >
< body >
< header >
< nav >...</ nav >
</ header >
< main >{children}</ main >
< footer >...</ footer >
</ body >
</ html >
);
}
// app/page.tsx - Home Page (Server Component)
export default async function HomePage () {
const posts = await db.post. findMany ({ take: 5 });
return (
< div >
< h1 >Latest Posts</ h1 >
< PostList posts = {posts} />
</ div >
);
}
// app/loading.tsx - Loading UI
export default function Loading () {
return < div >Loading...</ div >;
}
// app/error.tsx - Error Boundary (must be Client)
'use client' ;
export default function Error ({ error , reset }) {
return (
< div >
< h2 >Something went wrong!</ h2 >
< p >{error.message}</ p >
< button onClick = {reset}>Try again</ button >
</ div >
);
}
Example: Dynamic routes and data fetching
// app/blog/[slug]/page.tsx - Dynamic route
export default async function BlogPost ({ params }) {
// Params are automatically typed and available
const post = await db.post. findUnique ({
where: { slug: params.slug }
});
if ( ! post) {
notFound (); // Shows 404 page
}
return (
< article >
< h1 >{post.title}</ h1 >
< time >{post.publishedAt}</ time >
< div >{post.content}</ div >
</ article >
);
}
// Generate static params for SSG
export async function generateStaticParams () {
const posts = await db.post. findMany ();
return posts. map ( post => ({
slug: post.slug
}));
}
// Generate metadata
export async function generateMetadata ({ params }) {
const post = await db.post. findUnique ({
where: { slug: params.slug }
});
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage]
}
};
}
Example: Route handlers (API routes)
// app/api/users/route.ts - API Route Handler
import { NextResponse } from 'next/server' ;
// GET /api/users
export async function GET ( request ) {
const { searchParams } = new URL (request.url);
const page = searchParams. get ( 'page' ) || '1' ;
const users = await db.user. findMany ({
skip: ( parseInt (page) - 1 ) * 10 ,
take: 10
});
return NextResponse. json (users);
}
// POST /api/users
export async function POST ( request ) {
const body = await request. json ();
const user = await db.user. create ({
data: body
});
return NextResponse. json (user, { status: 201 });
}
// app/api/users/[id]/route.ts - Dynamic route
export async function GET ( request , { params }) {
const user = await db.user. findUnique ({
where: { id: params.id }
});
if ( ! user) {
return NextResponse. json ({ error: 'Not found' }, { status: 404 });
}
return NextResponse. json (user);
}
// PATCH /api/users/[id]
export async function PATCH ( request , { params }) {
const body = await request. json ();
const user = await db.user. update ({
where: { id: params.id },
data: body
});
return NextResponse. json (user);
}
// DELETE /api/users/[id]
export async function DELETE ( request , { params }) {
await db.user. delete ({
where: { id: params.id }
});
return NextResponse. json ({ success: true });
}
Next.js App Router Features: File-based routing, Server Components by default, automatic code
splitting, built-in loading states, error boundaries, metadata API, route handlers for APIs, streaming with
Suspense, TypeScript support.
17. React Testing Strategies and Patterns
17.1 React Testing Library Best Practices
Practice
Description
Example
Rationale
Test User Behavior
Test components as users interact with them, not implementation details
screen.getByRole('button')
Tests are resilient to refactoring and reflect real usage
Query Priority
Use accessible queries first: getByRole, getByLabelText, getByPlaceholderText, getByText
getByRole('textbox', {name: /email/i})
Ensures accessibility and semantic HTML
Avoid Test IDs
Use data-testid only as last resort when no semantic query available
getByTestId('custom-widget')
Encourages better accessibility practices
User-Event Library
Use @testing-library/user-event for realistic interactions
await user.click(button)
Simulates real browser events more accurately than fireEvent
Async Utilities
Use waitFor, findBy queries for async operations
await screen.findByText('Loaded')
Handles async updates and avoids race conditions
Avoid act() Warnings
Use RTL's async utilities instead of manual act() wrapping
await waitFor(() => expect(...))
Built-in utilities handle act() automatically
Example: Basic RTL test with best practices
// UserGreeting.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import UserGreeting from './UserGreeting' ;
describe ( 'UserGreeting' , () => {
it ( 'displays greeting after user enters name' , async () => {
const user = userEvent. setup ();
render (< UserGreeting />);
// Use accessible query
const input = screen. getByRole ( 'textbox' , { name: /your name/ i });
const button = screen. getByRole ( 'button' , { name: /submit/ i });
// Simulate user interaction
await user. type (input, 'Alice' );
await user. click (button);
// Assert user-visible outcome
expect ( await screen. findByText ( /hello, alice/ i )). toBeInTheDocument ();
});
});
Example: Query priority guide
// Preferred: Accessible to everyone
screen. getByRole ( 'button' , { name: /submit/ i })
screen. getByLabelText ( /email address/ i )
screen. getByPlaceholderText ( /search ... / i )
screen. getByText ( /welcome back/ i )
// Semantic queries
screen. getByAltText ( /company logo/ i )
screen. getByTitle ( /close dialog/ i )
// Test IDs (last resort)
screen. getByTestId ( 'custom-element' )
17.2 Component Testing and User Interactions
Test Type
Focus
Query Method
Example
Form Input
User typing and form submission
user.type(), user.click()
Type into input, submit form, verify result
Button Clicks
Click handlers and state changes
user.click()
Click button, verify UI update or API call
Async Data Loading
Fetch and display data
findBy*, waitFor()
Wait for loading state, then verify data displayed
Conditional Rendering
Show/hide elements based on state
queryBy*, expect().not.toBeInTheDocument()
Verify element appears/disappears correctly
Keyboard Navigation
Tab, Enter, Escape key handling
user.keyboard(), user.tab()
Navigate with keyboard, verify focus management
Selection Controls
Radio, checkbox, select interactions
user.selectOptions(), user.click()
Select options, verify state and behavior
// LoginForm.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import LoginForm from './LoginForm' ;
describe ( 'LoginForm' , () => {
it ( 'shows validation errors for invalid inputs' , async () => {
const user = userEvent. setup ();
const mockSubmit = jest. fn ();
render (< LoginForm onSubmit = {mockSubmit} />);
const emailInput = screen. getByLabelText ( /email/ i );
const passwordInput = screen. getByLabelText ( /password/ i );
const submitButton = screen. getByRole ( 'button' , { name: /log in/ i });
// Submit with invalid email
await user. type (emailInput, 'invalid-email' );
await user. type (passwordInput, '123' );
await user. click (submitButton);
// Verify validation errors appear
expect ( await screen. findByText ( /invalid email format/ i )). toBeInTheDocument ();
expect ( await screen. findByText ( /password must be at least 8 characters/ i )). toBeInTheDocument ();
expect (mockSubmit).not. toHaveBeenCalled ();
});
it ( 'submits form with valid inputs' , async () => {
const user = userEvent. setup ();
const mockSubmit = jest. fn ();
render (< LoginForm onSubmit = {mockSubmit} />);
await user. type (screen. getByLabelText ( /email/ i ), 'user@example.com' );
await user. type (screen. getByLabelText ( /password/ i ), 'securePassword123' );
await user. click (screen. getByRole ( 'button' , { name: /log in/ i }));
expect (mockSubmit). toHaveBeenCalledWith ({
email: 'user@example.com' ,
password: 'securePassword123'
});
});
});
Example: Testing async data fetching
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import UserProfile from './UserProfile' ;
import { fetchUser } from './api' ;
jest. mock ( './api' );
describe ( 'UserProfile' , () => {
it ( 'displays loading state then user data' , async () => {
fetchUser. mockResolvedValue ({
id: 1 ,
name: 'Alice' ,
email: 'alice@example.com'
});
render (< UserProfile userId = { 1 } />);
// Verify loading state
expect (screen. getByText ( /loading ... / i )). toBeInTheDocument ();
// Wait for data to load
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'alice@example.com' )). toBeInTheDocument ();
expect (screen. queryByText ( /loading ... / i )).not. toBeInTheDocument ();
});
it ( 'displays error message on fetch failure' , async () => {
fetchUser. mockRejectedValue ( new Error ( 'Network error' ));
render (< UserProfile userId = { 1 } />);
expect ( await screen. findByText ( /failed to load user/ i )). toBeInTheDocument ();
});
});
17.3 Hook Testing with renderHook
Hook Type
Testing Approach
Key Method
Example
Custom Hooks
Test hook logic in isolation
renderHook()
Test custom hook without wrapping component
State Updates
Test state changes and side effects
result.current, act()
Access hook values and trigger updates
Async Hooks
Test async operations in hooks
waitFor(() => expect(...))
Wait for async state updates
Hook Dependencies
Test effect dependencies and re-runs
rerender()
Change props and verify effect behavior
Context Hooks
Test hooks requiring context
wrapper: Provider
Wrap hook with necessary providers
Cleanup Testing
Verify cleanup functions run
unmount()
Test effect cleanup and subscriptions
Example: Testing custom hook with state
// useCounter.test.ts
import { renderHook, act } from '@testing-library/react' ;
import useCounter from './useCounter' ;
describe ( 'useCounter' , () => {
it ( 'initializes with default value' , () => {
const { result } = renderHook (() => useCounter ());
expect (result.current.count). toBe ( 0 );
});
it ( 'initializes with custom value' , () => {
const { result } = renderHook (() => useCounter ( 10 ));
expect (result.current.count). toBe ( 10 );
});
it ( 'increments count' , () => {
const { result } = renderHook (() => useCounter ());
act (() => {
result.current. increment ();
});
expect (result.current.count). toBe ( 1 );
});
it ( 'decrements count' , () => {
const { result } = renderHook (() => useCounter ( 5 ));
act (() => {
result.current. decrement ();
});
expect (result.current.count). toBe ( 4 );
});
it ( 'resets count to initial value' , () => {
const { result } = renderHook (() => useCounter ( 10 ));
act (() => {
result.current. increment ();
result.current. increment ();
result.current. reset ();
});
expect (result.current.count). toBe ( 10 );
});
});
Example: Testing async hook with API calls
// useFetchUser.test.ts
import { renderHook, waitFor } from '@testing-library/react' ;
import useFetchUser from './useFetchUser' ;
import { fetchUser } from './api' ;
jest. mock ( './api' );
describe ( 'useFetchUser' , () => {
it ( 'fetches user data successfully' , async () => {
const mockUser = { id: 1 , name: 'Alice' };
fetchUser. mockResolvedValue (mockUser);
const { result } = renderHook (() => useFetchUser ( 1 ));
// Initial state
expect (result.current.loading). toBe ( true );
expect (result.current.data). toBeNull ();
expect (result.current.error). toBeNull ();
// Wait for data to load
await waitFor (() => {
expect (result.current.loading). toBe ( false );
});
expect (result.current.data). toEqual (mockUser);
expect (result.current.error). toBeNull ();
});
it ( 'handles fetch errors' , async () => {
fetchUser. mockRejectedValue ( new Error ( 'Network error' ));
const { result } = renderHook (() => useFetchUser ( 1 ));
await waitFor (() => {
expect (result.current.loading). toBe ( false );
});
expect (result.current.data). toBeNull ();
expect (result.current.error). toEqual ( new Error ( 'Network error' ));
});
it ( 'refetches when userId changes' , async () => {
fetchUser. mockResolvedValue ({ id: 1 , name: 'Alice' });
const { result , rerender } = renderHook (
({ userId }) => useFetchUser (userId),
{ initialProps: { userId: 1 } }
);
await waitFor (() => expect (result.current.loading). toBe ( false ));
expect (fetchUser). toHaveBeenCalledWith ( 1 );
// Change userId
fetchUser. mockResolvedValue ({ id: 2 , name: 'Bob' });
rerender ({ userId: 2 });
await waitFor (() => expect (result.current.data.name). toBe ( 'Bob' ));
expect (fetchUser). toHaveBeenCalledWith ( 2 );
});
});
Example: Testing hook with context
// useAuth.test.tsx
import { renderHook } from '@testing-library/react' ;
import { AuthProvider } from './AuthContext' ;
import useAuth from './useAuth' ;
const wrapper = ({ children }) => (
< AuthProvider initialUser = {{ id: 1 , name: 'Alice' }}>
{children}
</ AuthProvider >
);
describe ( 'useAuth' , () => {
it ( 'provides auth context values' , () => {
const { result } = renderHook (() => useAuth (), { wrapper });
expect (result.current.user). toEqual ({ id: 1 , name: 'Alice' });
expect (result.current.isAuthenticated). toBe ( true );
});
it ( 'throws error when used outside provider' , () => {
// Suppress console.error for this test
const spy = jest. spyOn (console, 'error' ). mockImplementation (() => {});
expect (() => {
renderHook (() => useAuth ());
}). toThrow ( 'useAuth must be used within AuthProvider' );
spy. mockRestore ();
});
});
17.4 Mocking and Test Doubles for Components
Mock Type
Use Case
Implementation
Example
Module Mocking
Replace entire module with mock
jest.mock('./module')
Mock API clients, utilities
Function Mocking
Mock specific functions
jest.fn()
Mock callbacks, event handlers
Component Mocking
Replace child components
jest.mock('./Component')
Isolate component under test
Spy Functions
Track function calls without replacing
jest.spyOn(obj, 'method')
Verify method calls on objects
Timer Mocking
Control setTimeout, setInterval
jest.useFakeTimers()
Test debounce, throttle, delays
Network Mocking
Mock HTTP requests
MSW (Mock Service Worker)
Mock fetch, axios requests
Example: Mocking API calls
// TodoList.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import TodoList from './TodoList' ;
import * as api from './api' ;
// Mock the entire API module
jest. mock ( './api' );
describe ( 'TodoList' , () => {
beforeEach (() => {
jest. clearAllMocks ();
});
it ( 'loads and displays todos' , async () => {
const mockTodos = [
{ id: 1 , title: 'Buy milk' , completed: false },
{ id: 2 , title: 'Walk dog' , completed: true }
];
api.fetchTodos. mockResolvedValue (mockTodos);
render (< TodoList />);
expect ( await screen. findByText ( 'Buy milk' )). toBeInTheDocument ();
expect (screen. getByText ( 'Walk dog' )). toBeInTheDocument ();
expect (api.fetchTodos). toHaveBeenCalledTimes ( 1 );
});
it ( 'adds new todo' , async () => {
const user = userEvent. setup ();
api.fetchTodos. mockResolvedValue ([]);
api.createTodo. mockResolvedValue ({ id: 3 , title: 'New task' , completed: false });
render (< TodoList />);
const input = screen. getByPlaceholderText ( /new todo/ i );
await user. type (input, 'New task' );
await user. click (screen. getByRole ( 'button' , { name: /add/ i }));
await waitFor (() => {
expect (api.createTodo). toHaveBeenCalledWith ({ title: 'New task' });
});
expect ( await screen. findByText ( 'New task' )). toBeInTheDocument ();
});
});
Example: Mocking child components
// Dashboard.test.tsx
import { render, screen } from '@testing-library/react' ;
import Dashboard from './Dashboard' ;
// Mock complex child components
jest. mock ( './UserProfile' , () => {
return function MockUserProfile ({ userId }) {
return < div >User Profile: {userId}</ div >;
};
});
jest. mock ( './ActivityFeed' , () => {
return function MockActivityFeed () {
return < div >Activity Feed</ div >;
};
});
describe ( 'Dashboard' , () => {
it ( 'renders main layout with mocked children' , () => {
render (< Dashboard userId = { 123 } />);
expect (screen. getByText ( 'User Profile: 123' )). toBeInTheDocument ();
expect (screen. getByText ( 'Activity Feed' )). toBeInTheDocument ();
});
});
Example: Using Mock Service Worker (MSW)
// setupTests.ts
import { setupServer } from 'msw/node' ;
import { rest } from 'msw' ;
const handlers = [
rest. get ( '/api/users' , ( req , res , ctx ) => {
return res (
ctx. json ([
{ id: 1 , name: 'Alice' },
{ id: 2 , name: 'Bob' }
])
);
}),
];
export const server = setupServer ( ... handlers);
beforeAll (() => server. listen ());
afterEach (() => server. resetHandlers ());
afterAll (() => server. close ());
// UserList.test.tsx
import { render, screen } from '@testing-library/react' ;
import { server } from './setupTests' ;
import { rest } from 'msw' ;
import UserList from './UserList' ;
describe ( 'UserList' , () => {
it ( 'displays users from API' , async () => {
render (< UserList />);
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'Bob' )). toBeInTheDocument ();
});
it ( 'handles API errors' , async () => {
// Override handler for this test
server. use (
rest. get ( '/api/users' , ( req , res , ctx ) => {
return res (ctx. status ( 500 ), ctx. json ({ error: 'Server error' }));
})
);
render (< UserList />);
expect ( await screen. findByText ( /failed to load users/ i )). toBeInTheDocument ();
});
});
Example: Testing with fake timers
// SearchInput.test.tsx - testing debounced input
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import SearchInput from './SearchInput' ;
describe ( 'SearchInput with debounce' , () => {
beforeEach (() => {
jest. useFakeTimers ();
});
afterEach (() => {
jest. runOnlyPendingTimers ();
jest. useRealTimers ();
});
it ( 'debounces search callback' , async () => {
const mockSearch = jest. fn ();
const user = userEvent. setup ({ delay: null }); // Disable userEvent delays
render (< SearchInput onSearch = {mockSearch} debounceMs = { 500 } />);
const input = screen. getByRole ( 'textbox' );
// Type quickly
await user. type (input, 'test' );
// Callback not called yet
expect (mockSearch).not. toHaveBeenCalled ();
// Fast-forward time
jest. advanceTimersByTime ( 500 );
// Now callback is called once with final value
expect (mockSearch). toHaveBeenCalledTimes ( 1 );
expect (mockSearch). toHaveBeenCalledWith ( 'test' );
});
});
17.5 Integration Testing with React Router
Test Scenario
Setup
Key Utilities
Example
Route Rendering
Wrap with MemoryRouter
MemoryRouter
Test component renders at specific route
Navigation
Simulate link clicks
user.click(), screen queries
Click link, verify new route content
Route Parameters
Initialize with initialEntries
initialEntries: ['/user/123']
Test dynamic route params
Protected Routes
Test auth redirects
Navigate, conditional rendering
Verify redirect when unauthorized
History Manipulation
Create test history
createMemoryHistory()
Test back/forward navigation
Search Params
Include query strings
initialEntries: ['?tab=2']
Test URL search params handling
Example: Testing navigation and routes
// App.test.tsx
import { render, screen } from '@testing-library/react' ;
import userEvent from '@testing-library/user-event' ;
import { MemoryRouter } from 'react-router-dom' ;
import App from './App' ;
describe ( 'App Navigation' , () => {
it ( 'navigates from home to about page' , async () => {
const user = userEvent. setup ();
render (
< MemoryRouter initialEntries = {[ '/' ]}>
< App />
</ MemoryRouter >
);
// Verify home page
expect (screen. getByText ( /welcome home/ i )). toBeInTheDocument ();
// Click about link
const aboutLink = screen. getByRole ( 'link' , { name: /about/ i });
await user. click (aboutLink);
// Verify about page
expect (screen. getByText ( /about us/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /welcome home/ i )).not. toBeInTheDocument ();
});
it ( 'renders 404 page for unknown routes' , () => {
render (
< MemoryRouter initialEntries = {[ '/unknown-route' ]}>
< App />
</ MemoryRouter >
);
expect (screen. getByText ( /404 . * not found/ i )). toBeInTheDocument ();
});
});
Example: Testing dynamic routes with params
// UserProfile.test.tsx
import { render, screen, waitFor } from '@testing-library/react' ;
import { MemoryRouter, Route, Routes } from 'react-router-dom' ;
import UserProfile from './UserProfile' ;
import * as api from './api' ;
jest. mock ( './api' );
describe ( 'UserProfile' , () => {
it ( 'loads user data based on route param' , async () => {
const mockUser = { id: '123' , name: 'Alice' , email: 'alice@example.com' };
api.fetchUser. mockResolvedValue (mockUser);
render (
< MemoryRouter initialEntries = {[ '/user/123' ]}>
< Routes >
< Route path = "/user/:userId" element = {< UserProfile />} />
</ Routes >
</ MemoryRouter >
);
await waitFor (() => {
expect (api.fetchUser). toHaveBeenCalledWith ( '123' );
});
expect ( await screen. findByText ( 'Alice' )). toBeInTheDocument ();
expect (screen. getByText ( 'alice@example.com' )). toBeInTheDocument ();
});
});
Example: Testing protected routes
// ProtectedRoute.test.tsx
import { render, screen } from '@testing-library/react' ;
import { MemoryRouter } from 'react-router-dom' ;
import App from './App' ;
import { AuthProvider } from './AuthContext' ;
describe ( 'Protected Routes' , () => {
it ( 'redirects to login when not authenticated' , () => {
render (
< MemoryRouter initialEntries = {[ '/dashboard' ]}>
< AuthProvider initialAuth = {{ user: null , isAuthenticated: false }}>
< App />
</ AuthProvider >
</ MemoryRouter >
);
// Should be redirected to login
expect (screen. getByText ( /please log in/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /dashboard/ i )).not. toBeInTheDocument ();
});
it ( 'allows access when authenticated' , () => {
render (
< MemoryRouter initialEntries = {[ '/dashboard' ]}>
< AuthProvider initialAuth = {{
user: { id: 1 , name: 'Alice' },
isAuthenticated: true
}}>
< App />
</ AuthProvider >
</ MemoryRouter >
);
// Should show dashboard
expect (screen. getByText ( /dashboard/ i )). toBeInTheDocument ();
expect (screen. queryByText ( /please log in/ i )).not. toBeInTheDocument ();
});
});
17.6 End-to-End Testing with Cypress/Playwright
Tool
Strengths
Test Pattern
Use Case
Cypress
Developer-friendly, time-travel debugging, real-time reloading
cy.visit(), cy.get(), cy.click()
Full user flows, visual testing, network mocking
Playwright
Multi-browser, parallel execution, mobile emulation, auto-wait
page.goto(), page.click(), page.fill()
Cross-browser testing, mobile testing, API testing
Cypress Commands
Custom reusable commands
Cypress.Commands.add()
Login flows, common interactions
Network Interception
Mock/stub API responses
cy.intercept(), page.route()
Test error states, loading states
Visual Testing
Screenshot comparison
cy.screenshot(), expect(screenshot)
Detect visual regressions
Component Testing
Mount React components in test runner
cy.mount(), mount()
Component isolation with real browser
Example: Cypress E2E test
// cypress/e2e/login.cy.js
describe ( 'Login Flow' , () => {
beforeEach (() => {
cy. visit ( 'http://localhost:3000' );
});
it ( 'allows user to log in with valid credentials' , () => {
// Intercept API call
cy. intercept ( 'POST' , '/api/login' , {
statusCode: 200 ,
body: {
user: { id: 1 , name: 'Alice' , email: 'alice@example.com' },
token: 'fake-jwt-token'
}
}). as ( 'loginRequest' );
// Fill login form
cy. get ( 'input[name="email"]' ). type ( 'alice@example.com' );
cy. get ( 'input[name="password"]' ). type ( 'password123' );
cy. get ( 'button[type="submit"]' ). click ();
// Wait for API call
cy. wait ( '@loginRequest' );
// Verify redirect to dashboard
cy. url (). should ( 'include' , '/dashboard' );
cy. contains ( 'Welcome, Alice' ). should ( 'be.visible' );
// Verify token stored
cy. window (). its ( 'localStorage.token' ). should ( 'exist' );
});
it ( 'shows error message for invalid credentials' , () => {
cy. intercept ( 'POST' , '/api/login' , {
statusCode: 401 ,
body: { error: 'Invalid credentials' }
}). as ( 'loginRequest' );
cy. get ( 'input[name="email"]' ). type ( 'wrong@example.com' );
cy. get ( 'input[name="password"]' ). type ( 'wrongpassword' );
cy. get ( 'button[type="submit"]' ). click ();
cy. wait ( '@loginRequest' );
// Verify error message
cy. contains ( 'Invalid credentials' ). should ( 'be.visible' );
cy. url (). should ( 'include' , '/login' );
});
it ( 'validates form inputs' , () => {
cy. get ( 'button[type="submit"]' ). click ();
// Check validation errors
cy. contains ( 'Email is required' ). should ( 'be.visible' );
cy. contains ( 'Password is required' ). should ( 'be.visible' );
// Fill invalid email
cy. get ( 'input[name="email"]' ). type ( 'invalid-email' );
cy. get ( 'button[type="submit"]' ). click ();
cy. contains ( 'Invalid email format' ). should ( 'be.visible' );
});
});
Example: Cypress custom commands
// cypress/support/commands.js
Cypress.Commands. add ( 'login' , ( email , password ) => {
cy. visit ( '/login' );
cy. get ( 'input[name="email"]' ). type (email);
cy. get ( 'input[name="password"]' ). type (password);
cy. get ( 'button[type="submit"]' ). click ();
cy. url (). should ( 'include' , '/dashboard' );
});
Cypress.Commands. add ( 'logout' , () => {
cy. get ( '[data-testid="user-menu"]' ). click ();
cy. contains ( 'Logout' ). click ();
cy. url (). should ( 'include' , '/login' );
});
// Usage in tests
describe ( 'Dashboard' , () => {
beforeEach (() => {
cy. login ( 'alice@example.com' , 'password123' );
});
it ( 'displays user data' , () => {
cy. contains ( 'Welcome, Alice' ). should ( 'be.visible' );
});
it ( 'allows user to logout' , () => {
cy. logout ();
});
});
Example: Playwright E2E test
// tests/login.spec.ts
import { test, expect } from '@playwright/test' ;
test. describe ( 'Login Flow' , () => {
test. beforeEach ( async ({ page }) => {
await page. goto ( 'http://localhost:3000' );
});
test ( 'allows user to log in with valid credentials' , async ({ page }) => {
// Mock API response
await page. route ( '**/api/login' , async ( route ) => {
await route. fulfill ({
status: 200 ,
contentType: 'application/json' ,
body: JSON . stringify ({
user: { id: 1 , name: 'Alice' , email: 'alice@example.com' },
token: 'fake-jwt-token'
})
});
});
// Fill and submit form
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
// Verify redirect
await expect (page). toHaveURL ( / . * dashboard/ );
await expect (page. locator ( 'text=Welcome, Alice' )). toBeVisible ();
});
test ( 'handles network errors gracefully' , async ({ page }) => {
// Simulate network failure
await page. route ( '**/api/login' , ( route ) => route. abort ());
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
// Verify error message
await expect (page. locator ( 'text=/network error/i' )). toBeVisible ();
});
});
Example: Playwright with fixtures and page objects
// tests/fixtures.ts
import { test as base } from '@playwright/test' ;
// Create custom fixture for authenticated user
export const test = base. extend ({
authenticatedPage : async ({ page }, use ) => {
await page. goto ( 'http://localhost:3000/login' );
await page. fill ( 'input[name="email"]' , 'alice@example.com' );
await page. fill ( 'input[name="password"]' , 'password123' );
await page. click ( 'button[type="submit"]' );
await page. waitForURL ( '**/dashboard' );
await use (page);
}
});
// tests/pages/DashboardPage.ts
export class DashboardPage {
constructor ( private page ) {}
async navigateToSettings () {
await this .page. click ( '[data-testid="settings-link"]' );
}
async getUserName () {
return await this .page. locator ( '[data-testid="user-name"]' ). textContent ();
}
async createNewProject ( name : string ) {
await this .page. click ( 'button:has-text("New Project")' );
await this .page. fill ( 'input[name="project-name"]' , name);
await this .page. click ( 'button:has-text("Create")' );
}
}
// tests/dashboard.spec.ts
import { test } from './fixtures' ;
import { DashboardPage } from './pages/DashboardPage' ;
import { expect } from '@playwright/test' ;
test ( 'user can create new project' , async ({ authenticatedPage }) => {
const dashboard = new DashboardPage (authenticatedPage);
await dashboard. createNewProject ( 'My New Project' );
await expect (authenticatedPage. locator ( 'text=My New Project' )). toBeVisible ();
});
E2E Testing Best Practices: Start server before tests, use baseURL configuration, avoid
hard-coded waits (use auto-waiting), test critical user paths only, run in CI/CD pipeline, use parallel
execution, implement proper cleanup, mock external services, use data-testid for stable selectors, organize
tests by feature, maintain test independence.
Feature
Purpose
Access Method
Key Capabilities
Components Tab
Inspect component tree and props/state
Browser DevTools → React tab
View hierarchy, search components, inspect props/state/hooks
Profiler Tab
Measure rendering performance
Browser DevTools → Profiler tab
Record renders, flame graphs, ranked charts, commit details
Settings
Configure DevTools behavior
Gear icon in DevTools
Highlight updates, trace updates, component filters, theme
Highlight Updates
Visually show component re-renders
Settings → Highlight updates
Color-coded borders around re-rendering components
Strict Mode Badge
Indicates Strict Mode enabled
Component tree icons
Shows which components wrapped in StrictMode
Component Filters
Hide unwanted components from tree
Settings → Component filters
Filter by name, location, or HOC patterns
// Chrome Web Store
https : //chrome.google.com/webstore (search "React Developer Tools")
// Firefox Add-ons
https : //addons.mozilla.org (search "React Developer Tools")
// Edge Add-ons
https : //microsoftedge.microsoft.com/addons (search "React Developer Tools")
// Standalone (Electron app)
npm install - g react - devtools
react - devtools
// Connect standalone to app
< script src = "http://localhost:8097" ></ script >
// Enable highlight updates to see re-renders
// 1. Open React DevTools
// 2. Click Settings (gear icon)
// 3. Check "Highlight updates when components render"
// 4. Watch colored borders appear on updates
// Search for components
// 1. Click in Components tab
// 2. Press Cmd+F (Mac) or Ctrl+F (Windows)
// 3. Type component name
// 4. Navigate with Enter/Shift+Enter
// View component source
// 1. Select component in tree
// 2. Right panel shows props, state, hooks
// 3. Click "<>" icon to view source code
// 4. Opens in Sources tab at component definition
DevTools Shortcuts: Cmd/Ctrl + F (search), Cmd/Ctrl + P (search by component name), Click
component in page to select in tree (requires "Inspect" feature enabled), Click eye icon to inspect DOM node,
Use filter input to narrow tree view.
18.2 Component Inspector and Props Debugging
Inspector Feature
Information Shown
Interaction
Use Case
Props Panel
All props passed to component
View values, edit primitives
Debug incorrect prop values, test different prop combinations
State Panel
Component state values
View and edit state
Test different states, reproduce bugs, validate state logic
Hooks Panel
All hooks in component with values
View hook state, effects
Debug custom hooks, verify hook dependencies
Rendered By
Parent component that rendered this component
Click to navigate to parent
Understand component hierarchy and data flow
Owners
Component ownership chain
Shows entire owner hierarchy
Trace where props originated, debug context issues
Source Location
File and line number of component
Click to open in Sources tab
Quick navigation to component code
// Original component
function Counter () {
const [ count , setCount ] = useState ( 0 );
return < div >Count: {count}</ div >;
}
// In React DevTools:
// 1. Select Counter component
// 2. In right panel, see "State" section
// 3. Click on "count: 0" value
// 4. Edit to different value (e.g., 100)
// 5. Press Enter - component re-renders with new value
// Editing props (parent component needed)
function Parent () {
return < Child name = "Alice" age = { 25 } />;
}
// In DevTools:
// 1. Select Child component
// 2. See "Props" section: name: "Alice", age: 25
// 3. Edit primitive values directly
// 4. Note: Complex objects/functions can't be edited
function UserProfile () {
const [ loading , setLoading ] = useState ( true );
const [ user , setUser ] = useState ( null );
const [ error , setError ] = useState ( null );
useEffect (() => {
fetchUser (). then (setUser). catch (setError). finally (() => setLoading ( false ));
}, []);
const memoizedUser = useMemo (() => formatUser (user), [user]);
const handleClick = useCallback (() => console. log (user), [user]);
// ...
}
// In DevTools, Hooks section shows:
// State (0): true // loading
// State (1): null // user
// State (2): null // error
// Effect // useEffect
// Memo // useMemo
// Callback // useCallback
// You can:
// - Edit state values directly
// - See dependencies for useMemo/useCallback
// - Track which effects ran
// - Identify custom hooks by name
Example: Using "Rendered by" to trace component tree
// Component hierarchy
< App >
< Dashboard >
< UserPanel >
< UserAvatar src = {user.avatar} />
</ UserPanel >
</ Dashboard >
</ App >
// Select UserAvatar in DevTools
// "Rendered by" section shows:
// UserPanel → Dashboard → App
// Click any parent to navigate
// See full props chain:
// - App passes user to Dashboard
// - Dashboard passes user to UserPanel
// - UserPanel passes user.avatar to UserAvatar
Profiler Feature
Visualization
Metrics
Analysis Use
Flamegraph Chart
Hierarchical view of render times
Render duration per component
Identify slow components, bottlenecks in tree
Ranked Chart
Sorted list of components by render time
Duration, sorted slowest first
Quickly find most expensive renders
Component Chart
Timeline of individual component renders
Each commit, duration, props/state changes
Track re-render frequency, identify unnecessary renders
Commit Details
Specific render commit information
What triggered render, duration, timestamp
Understand why component re-rendered
Interactions
User interactions that triggered renders
Which user actions caused updates
Measure interaction responsiveness
Why Did This Render
Explanation of render cause
Props changed, state changed, parent rendered
Debug unnecessary re-renders
// Steps to profile your app:
// 1. Open React DevTools → Profiler tab
// 2. Click "Start Profiling" (blue record button)
// 3. Interact with your app (click buttons, type, navigate)
// 4. Click "Stop Profiling" (red square button)
// 5. Analyze the results
// Reading Flamegraph:
// - Wider bars = longer render time
// - Height = component depth in tree
// - Colors: Yellow (fast), Orange (medium), Red (slow)
// - Click bar to see component details
// Reading Ranked Chart:
// - Components sorted by render duration
// - Percentage shows relative time
// - Focus on top components first
// Example findings:
// Component: BigList
// Rendered: 15 times
// Duration: 1,245ms total
// Cause: Parent state change (unnecessary)
// Fix: Wrap in React.memo()
Example: Profiler API for custom measurements
import { Profiler } from 'react' ;
function onRenderCallback (
id , // "panel" - profiler ID
phase , // "mount" or "update"
actualDuration , // Time spent rendering
baseDuration , // Estimated time without memoization
startTime , // When render started
commitTime , // When React committed update
interactions // Set of interactions
) {
console. log ( `${ id } (${ phase }) took ${ actualDuration }ms` );
// Send to analytics
analytics. track ( 'component-render' , {
component: id,
phase,
duration: actualDuration,
wasSlower: actualDuration > baseDuration
});
}
function App () {
return (
< Profiler id = "dashboard" onRender = {onRenderCallback}>
< Dashboard />
</ Profiler >
);
}
// Nested profilers for granular tracking
function Dashboard () {
return (
< div >
< Profiler id = "sidebar" onRender = {onRenderCallback}>
< Sidebar />
</ Profiler >
< Profiler id = "content" onRender = {onRenderCallback}>
< Content />
</ Profiler >
</ div >
);
}
Example: Analyzing "Why Did This Render"
// Enable in Settings → General → "Record why each component rendered"
// Example analysis in DevTools:
// Component: TodoItem
// Rendered because: Props changed
// Changed props:
// - isCompleted: false → true
// - onClick: [function] → [function] (different reference!)
// Problem identified: onClick recreated on every render
// Before (bad):
function TodoList () {
const [ todos , setTodos ] = useState ([]);
return todos. map ( todo => (
< TodoItem
todo = {todo}
onClick = {() => handleToggle (todo.id)} // New function every render!
/>
));
}
// After (good):
function TodoList () {
const [ todos , setTodos ] = useState ([]);
const handleToggle = useCallback (( id ) => {
setTodos ( prev => prev. map ( t =>
t.id === id ? { ... t, completed: ! t.completed } : t
));
}, []);
return todos. map ( todo => (
< TodoItem
key = {todo.id}
todo = {todo}
onToggle = {handleToggle} // Stable reference
/>
));
}
const TodoItem = React. memo (({ todo , onToggle }) => (
< li onClick = {() => onToggle (todo.id)}>{todo.text}</ li >
));
18.4 State and Context Debugging Techniques
Technique
Tool/Method
What to Check
Common Issues Found
useState Inspection
DevTools Hooks panel
Current state values, update functions
Stale closures, incorrect initial state, batching issues
useReducer Debugging
DevTools + Redux DevTools
State shape, action history
Incorrect action handling, state mutations, missing cases
Context Values
DevTools Context panel
Current context value, provider location
Missing provider, wrong value, too many re-renders
State History
Custom logging, Redux DevTools
State changes over time
Unexpected mutations, missing updates
State Synchronization
Multiple component inspection
Shared state consistency
Race conditions, stale data, caching issues
Derived State
useMemo inspection
Memoized values, dependencies
Missing dependencies, over-computation
Example: Debugging context issues
// Context not updating problem
const UserContext = createContext ();
function App () {
const [ user , setUser ] = useState ({ name: 'Alice' , age: 25 });
// Problem: New object every render!
const value = { user, setUser };
return (
< UserContext.Provider value = {value}>
< Dashboard />
</ UserContext.Provider >
);
}
// In DevTools:
// 1. Select any consumer component
// 2. Check "Context" in right panel
// 3. See value changing every render (object identity)
// 4. All consumers re-render unnecessarily
// Fix:
function App () {
const [ user , setUser ] = useState ({ name: 'Alice' , age: 25 });
const value = useMemo (() => ({ user, setUser }), [user]);
return (
< UserContext.Provider value = {value}>
< Dashboard />
</ UserContext.Provider >
);
}
import { useReducer } from 'react' ;
// Install Redux DevTools Extension for full state tracking
function todoReducer ( state , action ) {
switch (action.type) {
case 'ADD' :
return [ ... state, action.payload];
case 'REMOVE' :
return state. filter ( todo => todo.id !== action.payload);
case 'TOGGLE' :
return state. map ( todo =>
todo.id === action.payload
? { ... todo, completed: ! todo.completed }
: todo
);
default :
return state;
}
}
// Connect useReducer to Redux DevTools
function useReducerWithDevTools ( reducer , initialState , name ) {
const [ state , dispatch ] = useReducer (reducer, initialState);
useEffect (() => {
if ( typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__) {
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__. connect ({ name });
devTools. init (initialState);
const wrappedDispatch = ( action ) => {
dispatch (action);
devTools. send (action, reducer (state, action));
};
return () => devTools. disconnect ();
}
}, []);
return [state, dispatch];
}
// Usage
function TodoApp () {
const [ todos , dispatch ] = useReducerWithDevTools (
todoReducer,
[],
'TodoApp'
);
// Now see all actions in Redux DevTools!
// - Time-travel debugging
// - Action history
// - State diff
}
Example: Custom state debugging hooks
// Development-only state logger
function useStateWithLogger ( initialState , name ) {
const [ state , setState ] = useState (initialState);
useEffect (() => {
if (process.env. NODE_ENV === 'development' ) {
console. log ( `[${ name }] State changed:` , state);
}
}, [state, name]);
return [state, setState];
}
// Track state change reasons
function useWhyDidYouUpdate ( name , props ) {
const previousProps = useRef ();
useEffect (() => {
if (previousProps.current) {
const allKeys = Object. keys ({ ... previousProps.current, ... props });
const changedProps = {};
allKeys. forEach ( key => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key]
};
}
});
if (Object. keys (changedProps). length > 0 ) {
console. log ( '[why-did-you-update]' , name, changedProps);
}
}
previousProps.current = props;
});
}
// Usage
function ExpensiveComponent ( props ) {
useWhyDidYouUpdate ( 'ExpensiveComponent' , props);
return < div >...</ div >;
}
18.5 Time-travel Debugging and Component History
Feature
Capability
Tool
Use Case
Profiler Timeline
Scrub through recorded renders
React DevTools Profiler
See component states at different render commits
Redux DevTools
Jump to any action in history
Redux DevTools Extension
Time-travel through state changes, replay actions
React Query DevTools
View cache history and mutations
@tanstack/react-query-devtools
Debug data fetching and caching issues
Custom History
Track component lifecycle events
Custom hooks with logging
Create audit trail of state changes
Browser DevTools
Record and replay interactions
Chrome Recorder, Replay.io
Reproduce bugs, share reproductions
Error Replay
Record session before errors
LogRocket, FullStory, Sentry
Debug production errors with context
Example: Using Profiler for time-travel debugging
// Record interaction flow:
// 1. Open React DevTools → Profiler
// 2. Click Record
// 3. Perform user actions (form submit, navigation, etc.)
// 4. Click Stop
// 5. Use timeline scrubber to navigate commits
// Each commit shows:
// - Which components rendered
// - Why they rendered (props/state change)
// - Render duration
// - Component props/state at that moment
// Navigate commits:
// - Left/Right arrows to step through
// - Click specific commit in timeline
// - See exact component state at that point
// - Compare before/after states
// Install Redux DevTools Extension
// Setup store with DevTools
import { createStore } from 'redux' ;
const store = createStore (
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window. __REDUX_DEVTOOLS_EXTENSION__ ()
);
// In Redux DevTools panel:
// 1. See all dispatched actions in order
// 2. Click any action to jump to that state
// 3. Use slider to scrub through action history
// 4. Toggle actions on/off to see impact
// 5. Export/import state snapshots
// 6. Replay actions at different speeds
// DevTools features:
// - Diff: See what changed in state
// - Test: Generate test code from actions
// - Export: Save state/action history as JSON
// - Import: Load previous session
// - Persist: Keep state across page reloads
Example: Building custom history tracker
// Create state history hook
function useStateHistory ( initialState , maxHistory = 50 ) {
const [ state , setState ] = useState (initialState);
const [ history , setHistory ] = useState ([initialState]);
const [ currentIndex , setCurrentIndex ] = useState ( 0 );
const updateState = useCallback (( newState ) => {
const value = typeof newState === 'function' ? newState (state) : newState;
setHistory ( prev => {
const newHistory = prev. slice ( 0 , currentIndex + 1 );
newHistory. push (value);
return newHistory. slice ( - maxHistory);
});
setCurrentIndex ( prev => Math. min (prev + 1 , maxHistory - 1 ));
setState (value);
}, [state, currentIndex, maxHistory]);
const undo = useCallback (() => {
if (currentIndex > 0 ) {
const newIndex = currentIndex - 1 ;
setCurrentIndex (newIndex);
setState (history[newIndex]);
}
}, [currentIndex, history]);
const redo = useCallback (() => {
if (currentIndex < history. length - 1 ) {
const newIndex = currentIndex + 1 ;
setCurrentIndex (newIndex);
setState (history[newIndex]);
}
}, [currentIndex, history]);
const canUndo = currentIndex > 0 ;
const canRedo = currentIndex < history. length - 1 ;
return {
state,
setState: updateState,
undo,
redo,
canUndo,
canRedo,
history,
currentIndex
};
}
// Usage - Text editor with undo/redo
function TextEditor () {
const {
state : text ,
setState : setText ,
undo ,
redo ,
canUndo ,
canRedo
} = useStateHistory ( '' );
return (
< div >
< button onClick = {undo} disabled = { ! canUndo}>Undo</ button >
< button onClick = {redo} disabled = { ! canRedo}>Redo</ button >
< textarea
value = {text}
onChange = {( e ) => setText (e.target.value)}
/>
</ div >
);
}
18.6 Production Debugging and Error Tracking
Tool/Service
Capabilities
Key Features
Best For
Sentry
Error tracking, performance monitoring
Error grouping, source maps, breadcrumbs, releases
Production error tracking and alerting
LogRocket
Session replay, error tracking
Video replay, network logs, console logs, Redux state
Visual debugging with full session context
Bugsnag
Error monitoring and reporting
Stability score, release tracking, user impact analysis
Error management and prioritization
Datadog RUM
Real user monitoring
Performance metrics, session replay, error tracking, analytics
Full-stack monitoring and observability
React Error Boundary
Catch React errors in production
Fallback UI, error reporting integration
Graceful error handling in app
Source Maps
Map minified code to source
Readable stack traces in production
Debug minified production code
Example: Sentry setup for React
// Install
npm install @sentry / react
// Initialize in entry point
import * as Sentry from '@sentry/react' ;
import { BrowserTracing } from '@sentry/tracing' ;
Sentry. init ({
dsn: 'YOUR_SENTRY_DSN' ,
integrations: [
new BrowserTracing (),
],
tracesSampleRate: 1.0 , // Sample rate for performance
environment: process.env. NODE_ENV ,
release: process.env. REACT_APP_VERSION ,
// Upload source maps for readable stack traces
beforeSend ( event , hint ) {
// Filter sensitive data
if (event.request) {
delete event.request.cookies;
}
return event;
}
});
// Wrap app with Sentry ErrorBoundary
function App () {
return (
< Sentry.ErrorBoundary fallback = {ErrorFallback} showDialog >
< YourApp />
</ Sentry.ErrorBoundary >
);
}
// Add user context
Sentry. setUser ({
id: user.id,
email: user.email,
username: user.username
});
// Add breadcrumbs for debugging
Sentry. addBreadcrumb ({
category: 'navigation' ,
message: 'User navigated to dashboard' ,
level: 'info'
});
// Manual error capture
try {
riskyOperation ();
} catch (error) {
Sentry. captureException (error, {
tags: {
section: 'payment' ,
severity: 'critical'
},
contexts: {
transaction: {
id: transactionId,
amount: amount
}
}
});
}
Example: LogRocket integration
// Install
npm install logrocket logrocket - react
// Initialize
import LogRocket from 'logrocket' ;
import setupLogRocketReact from 'logrocket-react' ;
LogRocket. init ( 'YOUR_APP_ID' );
setupLogRocketReact (LogRocket);
// Identify users
LogRocket. identify (user.id, {
name: user.name,
email: user.email,
subscriptionType: user.plan
});
// Integration with Sentry
import * as Sentry from '@sentry/react' ;
LogRocket. getSessionURL ( sessionURL => {
Sentry. configureScope ( scope => {
scope. setExtra ( 'sessionURL' , sessionURL);
});
});
// Track custom events
LogRocket. track ( 'Purchase Completed' , {
productId: product.id,
revenue: price,
category: product.category
});
// Sanitize sensitive data
LogRocket. init ( 'YOUR_APP_ID' , {
dom: {
inputSanitizer: true , // Sanitize input values
},
network: {
requestSanitizer : request => {
// Remove sensitive headers
if (request.headers[ 'Authorization' ]) {
request.headers[ 'Authorization' ] = 'REDACTED' ;
}
return request;
},
responseSanitizer : response => {
// Remove sensitive response data
if (response.body && response.body.creditCard) {
response.body.creditCard = 'REDACTED' ;
}
return response;
}
}
});
Example: Production error boundary with reporting
import { Component } from 'react' ;
import * as Sentry from '@sentry/react' ;
class ErrorBoundary extends Component {
constructor ( props ) {
super (props);
this .state = { hasError: false , error: null };
}
static getDerivedStateFromError ( error ) {
return { hasError: true , error };
}
componentDidCatch ( error , errorInfo ) {
// Log to console in development
if (process.env. NODE_ENV === 'development' ) {
console. error ( 'Error caught by boundary:' , error, errorInfo);
}
// Report to error tracking service
Sentry. captureException (error, {
contexts: {
react: {
componentStack: errorInfo.componentStack
}
}
});
// Log to analytics
analytics. track ( 'error_boundary_triggered' , {
error: error.message,
component: errorInfo.componentStack. split ( ' \n ' )[ 1 ]
});
}
render () {
if ( this .state.hasError) {
return this .props.fallback || (
< div role = "alert" >
< h2 >Something went wrong</ h2 >
< details style = {{ whiteSpace: 'pre-wrap' }}>
{ this .state.error && this .state.error. toString ()}
</ details >
< button onClick = {() => window.location. reload ()}>
Reload Page
</ button >
</ div >
);
}
return this .props.children;
}
}
Example: Source map configuration for production debugging
// Create React App - .env.production
GENERATE_SOURCEMAP = true
// Upload source maps to Sentry
// package.json
{
"scripts" : {
"build" : "react-scripts build" ,
"sentry:upload" : "sentry-cli releases files $npm_package_version upload-sourcemaps ./build"
}
}
// Webpack config (for custom setup)
module . exports = {
devtool: 'source-map' , // Generate source maps
plugins: [
new SentryWebpackPlugin ({
include: './build' ,
ignoreFile: '.sentrycliignore' ,
ignore: [ 'node_modules' , 'webpack.config.js' ],
configFile: 'sentry.properties' ,
release: process.env. REACT_APP_VERSION
})
]
};
// .sentryignore
node_modules /
* .map
// Only upload source maps, don't serve them publicly
// Serve source maps only to authenticated users or keep them private
// Configure error tracking to fetch maps from your servers
Production Debugging Best Practices: Always use error boundaries, upload source maps to error
tracking service, sanitize sensitive data before logging, add user context to errors, implement breadcrumbs for
error context, monitor error rates and trends, set up alerts for critical errors, test error tracking in
staging, use session replay for complex bugs, correlate errors with releases.
19. React Ecosystem Integration
19.1 React Router and Navigation Patterns
Feature
API
Example
Use Case
Basic Routing
BrowserRouter, Routes, Route
<Route path="/about" element={<About />} />
Define app routes and components
Dynamic Routes
:param syntax, useParams()
<Route path="/user/:id" />
Routes with variable segments
Navigation
Link, NavLink, useNavigate()
<Link to="/about">About</Link>
Navigate between routes
Nested Routes
Outlet, nested Route
<Route path="dashboard" element={<Layout />}>
Layout with child routes
Protected Routes
Navigate, conditional rendering
{!auth ? <Navigate to="/login" /> : children}
Restrict access by auth status
Query Params
useSearchParams()
const [params, setParams] = useSearchParams()
Read/write URL query strings
Lazy Loading
React.lazy(), Suspense
const About = lazy(() => import('./About'))
Code-split route components
Example: React Router v6 setup
// Install
npm install react - router - dom
// App.tsx
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom' ;
function App () {
return (
< BrowserRouter >
< nav >
< Link to = "/" >Home</ Link >
< Link to = "/about" >About</ Link >
< Link to = "/users" >Users</ Link >
</ nav >
< Routes >
< Route path = "/" element = {< Home />} />
< Route path = "/about" element = {< About />} />
< Route path = "/users" element = {< Users />} />
< Route path = "/users/:id" element = {< UserProfile />} />
< Route path = "*" element = {< NotFound />} />
</ Routes >
</ BrowserRouter >
);
}
// Dynamic route with useParams
function UserProfile () {
const { id } = useParams ();
return < div >User Profile: {id}</ div >;
}
// Programmatic navigation
function LoginButton () {
const navigate = useNavigate ();
const handleLogin = async () => {
await login ();
navigate ( '/dashboard' , { replace: true });
};
return < button onClick = {handleLogin}>Login</ button >;
}
Example: Nested routes and layouts
import { Outlet } from 'react-router-dom' ;
// Layout component with Outlet
function DashboardLayout () {
return (
< div >
< header >Dashboard Header</ header >
< aside >Sidebar</ aside >
< main >
< Outlet /> { /* Child routes render here */ }
</ main >
</ div >
);
}
// App with nested routes
function App () {
return (
< Routes >
< Route path = "/dashboard" element = {< DashboardLayout />}>
< Route index element = {< DashboardHome />} />
< Route path = "profile" element = {< Profile />} />
< Route path = "settings" element = {< Settings />} />
</ Route >
</ Routes >
);
}
// Protected route wrapper
function ProtectedRoute ({ children }) {
const { isAuthenticated } = useAuth ();
if ( ! isAuthenticated) {
return < Navigate to = "/login" replace />;
}
return children;
}
// Usage
< Route
path = "/dashboard"
element = {
< ProtectedRoute >
< DashboardLayout />
</ ProtectedRoute >
}
/>
19.2 State Management Libraries (Redux, Zustand)
Library
Key Features
Core API
Best For
Redux Toolkit
Reducers, actions, middleware, DevTools
configureStore, createSlice, createAsyncThunk
Large apps, complex state, predictable updates
Zustand
Minimal, no boilerplate, React-first
create(), set(), get()
Simple global state, quick setup, small apps
Jotai
Atomic state, bottom-up approach
atom(), useAtom()
Fine-grained reactivity, derived state
Recoil
Atoms, selectors, async state
atom(), selector(), useRecoilState()
Complex derived state, async dependencies
MobX
Observable state, automatic tracking
makeObservable, observer, action
OOP patterns, automatic reactivity
Valtio
Proxy-based, mutable syntax
proxy(), useSnapshot()
Mutable-style state, simple API
// Install
npm install @reduxjs / toolkit react - redux
// store.ts
import { configureStore } from '@reduxjs/toolkit' ;
import counterReducer from './counterSlice' ;
export const store = configureStore ({
reducer: {
counter: counterReducer,
},
});
// counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit' ;
interface CounterState {
value : number ;
}
const initialState : CounterState = { value: 0 };
const counterSlice = createSlice ({
name: 'counter' ,
initialState,
reducers: {
increment : ( state ) => {
state.value += 1 ; // Immer makes this safe
},
decrement : ( state ) => {
state.value -= 1 ;
},
incrementByAmount : ( state , action : PayloadAction < number >) => {
state.value += action.payload;
},
},
});
export const { increment , decrement , incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
// App.tsx
import { Provider } from 'react-redux' ;
import { store } from './store' ;
function App () {
return (
< Provider store = {store}>
< Counter />
</ Provider >
);
}
// Counter.tsx
import { useSelector, useDispatch } from 'react-redux' ;
import { increment, decrement } from './counterSlice' ;
function Counter () {
const count = useSelector (( state ) => state.counter.value);
const dispatch = useDispatch ();
return (
< div >
< button onClick = {() => dispatch ( decrement ())}>-</ button >
< span >{count}</ span >
< button onClick = {() => dispatch ( increment ())}>+</ button >
</ div >
);
}
Example: Zustand - minimal state management
// Install
npm install zustand
// store.ts
import { create } from 'zustand' ;
import { devtools, persist } from 'zustand/middleware' ;
interface BearStore {
bears : number ;
increase : () => void ;
decrease : () => void ;
reset : () => void ;
}
export const useBearStore = create < BearStore >()(
devtools (
persist (
( set ) => ({
bears: 0 ,
increase : () => set (( state ) => ({ bears: state.bears + 1 })),
decrease : () => set (( state ) => ({ bears: state.bears - 1 })),
reset : () => set ({ bears: 0 }),
}),
{ name: 'bear-storage' }
)
)
);
// Component.tsx
function BearCounter () {
const bears = useBearStore (( state ) => state.bears);
return < h1 >{bears} bears</ h1 >;
}
function Controls () {
const increase = useBearStore (( state ) => state.increase);
const decrease = useBearStore (( state ) => state.decrease);
return (
< div >
< button onClick = {decrease}>-</ button >
< button onClick = {increase}>+</ button >
</ div >
);
}
// Async actions
interface UserStore {
user : User | null ;
fetchUser : ( id : string ) => Promise < void >;
}
export const useUserStore = create < UserStore >(( set ) => ({
user: null ,
fetchUser : async ( id ) => {
const response = await fetch ( `/api/users/${ id }` );
const user = await response. json ();
set ({ user });
},
}));
19.3 UI Component Libraries Integration
Library
Design System
Key Features
Bundle Size
Material-UI (MUI)
Material Design
Comprehensive components, theming, customization, TypeScript
Large (~300KB min+gzip)
Ant Design
Enterprise UI
200+ components, i18n, form validation, charts
Large (~500KB min+gzip)
Chakra UI
Accessible, composable
Style props, dark mode, responsive, a11y-first
Medium (~150KB min+gzip)
Mantine
Modern, feature-rich
120+ components, hooks, form management, notifications
Medium (~180KB min+gzip)
shadcn/ui
Copy-paste components
Radix UI + Tailwind, full control, no package dependency
Small (only what you use)
Radix UI
Headless components
Unstyled, accessible primitives, full control
Small (~50KB per component)
Headless UI
Tailwind Labs
Unstyled, accessible, works with Tailwind
Small (~20KB min+gzip)
Example: Material-UI (MUI) setup
// Install
npm install @mui / material @emotion / react @emotion / styled
// App.tsx
import { ThemeProvider, createTheme } from '@mui/material/styles' ;
import { Button, Container, Typography, Box } from '@mui/material' ;
import CssBaseline from '@mui/material/CssBaseline' ;
const theme = createTheme ({
palette: {
primary: {
main: '#1976d2' ,
},
secondary: {
main: '#dc004e' ,
},
},
});
function App () {
return (
< ThemeProvider theme = {theme}>
< CssBaseline />
< Container >
< Typography variant = "h1" component = "h1" gutterBottom >
Hello MUI
</ Typography >
< Box sx = {{ display: 'flex' , gap: 2 }}>
< Button variant = "contained" color = "primary" >
Primary
</ Button >
< Button variant = "outlined" color = "secondary" >
Secondary
</ Button >
</ Box >
</ Container >
</ ThemeProvider >
);
}
Example: Chakra UI setup
// Install
npm install @chakra - ui / react @emotion / react @emotion / styled framer - motion
// App.tsx
import { ChakraProvider, Box, Button, Heading, VStack } from '@chakra-ui/react' ;
import { extendTheme } from '@chakra-ui/react' ;
const theme = extendTheme ({
colors: {
brand: {
50 : '#e3f2fd' ,
500 : '#2196f3' ,
900 : '#0d47a1' ,
},
},
});
function App () {
return (
< ChakraProvider theme = {theme}>
< Box p = { 8 }>
< VStack spacing = { 4 } align = "stretch" >
< Heading size = "xl" >Hello Chakra</ Heading >
< Button colorScheme = "brand" size = "lg" >
Click Me
</ Button >
< Box bg = "gray.100" p = { 4 } borderRadius = "md" >
Responsive Box
</ Box >
</ VStack >
</ Box >
</ ChakraProvider >
);
}
Example: shadcn/ui - copy-paste approach
// Install CLI
npx shadcn - ui@latest init
// Add components
npx shadcn - ui@latest add button
npx shadcn - ui@latest add card
// Components are copied to your project
// components/ui/button.tsx
// components/ui/card.tsx
// Usage
import { Button } from '@/components/ui/button' ;
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card' ;
function App () {
return (
< Card >
< CardHeader >
< CardTitle >Welcome</ CardTitle >
</ CardHeader >
< CardContent >
< Button variant = "default" >Click Me</ Button >
< Button variant = "outline" >Outline</ Button >
</ CardContent >
</ Card >
);
}
// Full control - edit copied components directly
// Tailwind-based styling
// No package lock-in
19.4 CSS-in-JS Solutions and Styling
Solution
Approach
Features
Performance
styled-components
Tagged templates
Automatic vendor prefixing, theming, SSR, TypeScript
Runtime CSS generation
Emotion
Tagged templates or object
Fast, flexible, source maps, framework agnostic
Runtime, faster than styled-components
Tailwind CSS
Utility classes
JIT compiler, purging, plugins, mobile-first
Zero runtime, build-time
CSS Modules
Scoped CSS files
Local scope, composition, type-safe with TypeScript
Zero runtime, standard CSS
Vanilla Extract
Zero-runtime CSS-in-TS
Type-safe, theme contracts, build-time extraction
Zero runtime, static CSS
Linaria
Zero-runtime CSS-in-JS
Build-time extraction, critical CSS, SSR
Zero runtime, extracted CSS
Panda CSS
Build-time atomic CSS
Type-safe, recipes, variants, zero runtime
Zero runtime, optimized output
Example: styled-components
// Install
npm install styled - components
// Component.tsx
import styled from 'styled-components' ;
const Button = styled. button `
background: ${ props => props . primary ? '#007bff' : '#6c757d'};
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
` ;
const Container = styled. div `
max-width: 1200px;
margin: 0 auto;
padding: 20px;
@media (max-width: 768px) {
padding: 10px;
}
` ;
// Usage
function App () {
return (
< Container >
< Button primary >Primary</ Button >
< Button >Secondary</ Button >
</ Container >
);
}
// Theming
import { ThemeProvider } from 'styled-components' ;
const theme = {
colors: {
primary: '#007bff' ,
secondary: '#6c757d' ,
},
spacing: {
small: '8px' ,
medium: '16px' ,
},
};
const ThemedButton = styled. button `
background: ${ props => props . theme . colors . primary };
padding: ${ props => props . theme . spacing . medium };
` ;
function App () {
return (
< ThemeProvider theme = {theme}>
< ThemedButton >Themed</ ThemedButton >
</ ThemeProvider >
);
}
Example: Tailwind CSS with React
// Install
npm install - D tailwindcss postcss autoprefixer
npx tailwindcss init - p
// tailwind.config.js
module . exports = {
content: [ './src/**/*.{js,jsx,ts,tsx}' ],
theme: {
extend: {
colors: {
brand: '#007bff' ,
},
},
},
plugins: [],
};
// index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// Component.tsx
function Button ({ children , variant = 'primary' }) {
const baseClasses = 'px-4 py-2 rounded font-medium transition' ;
const variantClasses = {
primary: 'bg-blue-500 text-white hover:bg-blue-600' ,
secondary: 'bg-gray-500 text-white hover:bg-gray-600' ,
outline: 'border border-blue-500 text-blue-500 hover:bg-blue-50' ,
};
return (
< button className = { `${ baseClasses } ${ variantClasses [ variant ] }` }>
{children}
</ button >
);
}
// With clsx for conditional classes
import clsx from 'clsx' ;
function Card ({ children , active }) {
return (
< div className = { clsx (
'p-4 rounded-lg shadow' ,
active ? 'bg-blue-100 border-blue-500' : 'bg-white border-gray-200' ,
'border-2 transition-colors'
)}>
{children}
</ div >
);
}
Example: CSS Modules
// Button.module.css
.button {
padding : 10px 20px;
border : none;
border - radius : 4px;
cursor : pointer;
transition : opacity 0.2s;
}
.button:hover {
opacity : 0.8 ;
}
.primary {
background : #007bff;
color : white;
}
.secondary {
background : #6c757d;
color : white;
}
.button:disabled {
opacity : 0.5 ;
cursor : not - allowed;
}
// Button.tsx
import styles from './Button.module.css' ;
import clsx from 'clsx' ;
interface ButtonProps {
variant ?: 'primary' | 'secondary' ;
disabled ?: boolean ;
children : React . ReactNode ;
}
function Button ({ variant = 'primary' , disabled , children } : ButtonProps ) {
return (
< button
className = { clsx (styles.button, styles[variant])}
disabled = {disabled}
>
{children}
</ button >
);
}
// Composition
.base {
padding : 10px;
}
.large {
composes : base;
padding : 20px;
font - size : 18px;
}
Library
Key Features
Validation
Best For
React Hook Form
Performance-focused, minimal re-renders, small bundle
Built-in, Yup, Zod, Joi integration
Complex forms, performance-critical apps
Formik
Popular, full-featured, field-level validation
Built-in, Yup integration
Standard forms, familiar API
Final Form
Framework-agnostic, subscription-based updates
Custom validators, async validation
Flexible form state management
Zod
TypeScript-first schema validation
Type inference, parse, transform
TypeScript projects, type-safe forms
Yup
Schema-based validation, chainable API
Sync/async validation, custom rules
Form validation, data validation
TanStack Form
Framework-agnostic, type-safe, adapters
Custom validators, async, field-level
Modern forms, framework flexibility
// Install
npm install react - hook - form @hookform / resolvers zod
// LoginForm.tsx
import { useForm } from 'react-hook-form' ;
import { zodResolver } from '@hookform/resolvers/zod' ;
import { z } from 'zod' ;
// Define schema
const loginSchema = z. object ({
email: z. string (). email ( 'Invalid email address' ),
password: z. string (). min ( 8 , 'Password must be at least 8 characters' ),
rememberMe: z. boolean (). optional (),
});
type LoginFormData = z . infer < typeof loginSchema>;
function LoginForm () {
const {
register ,
handleSubmit ,
formState : { errors , isSubmitting },
} = useForm < LoginFormData >({
resolver: zodResolver (loginSchema),
});
const onSubmit = async ( data : LoginFormData ) => {
await loginUser (data);
};
return (
< form onSubmit = { handleSubmit (onSubmit)}>
< div >
< label htmlFor = "email" >Email</ label >
< input
{ ... register ( 'email' )}
type = "email"
id = "email"
/>
{errors.email && < span >{errors.email.message}</ span >}
</ div >
< div >
< label htmlFor = "password" >Password</ label >
< input
{ ... register ( 'password' )}
type = "password"
id = "password"
/>
{errors.password && < span >{errors.password.message}</ span >}
</ div >
< div >
< label >
< input { ... register ( 'rememberMe' )} type = "checkbox" />
Remember Me
</ label >
</ div >
< button type = "submit" disabled = {isSubmitting}>
{isSubmitting ? 'Logging in...' : 'Login' }
</ button >
</ form >
);
}
Example: Dynamic fields and arrays
import { useForm, useFieldArray } from 'react-hook-form' ;
const schema = z. object ({
name: z. string (). min ( 1 ),
emails: z. array (
z. object ({
value: z. string (). email (),
primary: z. boolean (),
})
). min ( 1 , 'At least one email required' ),
});
function ProfileForm () {
const { register , control , handleSubmit , formState : { errors } } = useForm ({
resolver: zodResolver (schema),
defaultValues: {
name: '' ,
emails: [{ value: '' , primary: true }],
},
});
const { fields , append , remove } = useFieldArray ({
control,
name: 'emails' ,
});
return (
< form onSubmit = { handleSubmit (onSubmit)}>
< input { ... register ( 'name' )} placeholder = "Name" />
{errors.name && < span >{errors.name.message}</ span >}
{fields. map (( field , index ) => (
< div key = {field.id}>
< input
{ ... register ( `emails.${ index }.value` )}
placeholder = "Email"
/>
< label >
< input
{ ... register ( `emails.${ index }.primary` )}
type = "checkbox"
/>
Primary
</ label >
< button type = "button" onClick = {() => remove (index)}>
Remove
</ button >
</ div >
))}
< button type = "button" onClick = {() => append ({ value: '' , primary: false })}>
Add Email
</ button >
< button type = "submit" >Submit</ button >
</ form >
);
}
19.6 Data Fetching Libraries (SWR, React Query)
Library
Features
Key Benefits
Use Case
TanStack Query (React Query)
Caching, refetching, pagination, infinite scroll, mutations
Automatic background updates, optimistic updates, devtools
Complex data fetching, real-time updates
SWR
Stale-while-revalidate, focus revalidation, lightweight
Simple API, automatic revalidation, fast
Simple data fetching, real-time apps
Apollo Client
GraphQL-focused, caching, subscriptions, local state
GraphQL integration, normalized cache, tooling
GraphQL APIs, complex data graphs
RTK Query
Redux Toolkit integration, auto-generated hooks
Redux integration, code generation, TypeScript
Redux apps, REST/GraphQL APIs
tRPC
End-to-end type safety, React Query integration
No code generation, type inference, full-stack
TypeScript full-stack apps
Example: React Query (TanStack Query)
// Install
npm install @tanstack / react - query
// Setup
import { QueryClient, QueryClientProvider } from '@tanstack/react-query' ;
import { ReactQueryDevtools } from '@tanstack/react-query-devtools' ;
const queryClient = new QueryClient ({
defaultOptions: {
queries: {
staleTime: 60 * 1000 , // 1 minute
refetchOnWindowFocus: false ,
},
},
});
function App () {
return (
< QueryClientProvider client = {queryClient}>
< YourApp />
< ReactQueryDevtools initialIsOpen = { false } />
</ QueryClientProvider >
);
}
// Basic query
import { useQuery } from '@tanstack/react-query' ;
function UserProfile ({ userId }) {
const { data , isLoading , error } = useQuery ({
queryKey: [ 'user' , userId],
queryFn : () => fetchUser (userId),
});
if (isLoading) return < div >Loading...</ div >;
if (error) return < div >Error: {error.message}</ div >;
return < div >{data.name}</ div >;
}
// Mutations
import { useMutation, useQueryClient } from '@tanstack/react-query' ;
function CreatePost () {
const queryClient = useQueryClient ();
const mutation = useMutation ({
mutationFn: createPost,
onSuccess : () => {
// Invalidate and refetch
queryClient. invalidateQueries ({ queryKey: [ 'posts' ] });
},
});
return (
< button onClick = {() => mutation. mutate ({ title: 'New Post' })}>
Create Post
</ button >
);
}
// Optimistic updates
const mutation = useMutation ({
mutationFn: updateTodo,
onMutate : async ( newTodo ) => {
await queryClient. cancelQueries ({ queryKey: [ 'todos' ] });
const previousTodos = queryClient. getQueryData ([ 'todos' ]);
queryClient. setQueryData ([ 'todos' ], ( old ) => [ ... old, newTodo]);
return { previousTodos };
},
onError : ( err , newTodo , context ) => {
queryClient. setQueryData ([ 'todos' ], context.previousTodos);
},
onSettled : () => {
queryClient. invalidateQueries ({ queryKey: [ 'todos' ] });
},
});
Example: SWR - simple data fetching
// Install
npm install swr
// Basic usage
import useSWR from 'swr' ;
const fetcher = ( url ) => fetch (url). then ( res => res. json ());
function Profile () {
const { data , error , isLoading } = useSWR ( '/api/user' , fetcher);
if (error) return < div >Failed to load</ div >;
if (isLoading) return < div >Loading...</ div >;
return < div >Hello {data.name}!</ div >;
}
// Global config
import { SWRConfig } from 'swr' ;
function App () {
return (
< SWRConfig
value = {{
fetcher : ( url ) => fetch (url). then ( res => res. json ()),
refreshInterval: 3000 ,
revalidateOnFocus: true ,
}}
>
< Dashboard />
</ SWRConfig >
);
}
// Mutations
import useSWRMutation from 'swr/mutation' ;
async function updateUser ( url , { arg }) {
await fetch (url, {
method: 'PUT' ,
body: JSON . stringify (arg)
});
}
function Settings () {
const { trigger , isMutating } = useSWRMutation ( '/api/user' , updateUser);
return (
< button
disabled = {isMutating}
onClick = {() => trigger ({ name: 'New Name' })}
>
Update User
</ button >
);
}
// Pagination
function Projects () {
const [ pageIndex , setPageIndex ] = useState ( 0 );
const { data } = useSWR ( `/api/projects?page=${ pageIndex }` , fetcher);
return (
< div >
{data?.projects. map ( p => < div key = {p.id}>{p.name}</ div >)}
< button onClick = {() => setPageIndex (pageIndex - 1 )}>Previous</ button >
< button onClick = {() => setPageIndex (pageIndex + 1 )}>Next</ button >
</ div >
);
}
import { useInfiniteQuery } from '@tanstack/react-query' ;
import { useInView } from 'react-intersection-observer' ;
function Posts () {
const { ref , inView } = useInView ();
const {
data ,
fetchNextPage ,
hasNextPage ,
isFetchingNextPage ,
} = useInfiniteQuery ({
queryKey: [ 'posts' ],
queryFn : ({ pageParam = 0 }) => fetchPosts (pageParam),
getNextPageParam : ( lastPage , pages ) => lastPage.nextCursor,
});
useEffect (() => {
if (inView && hasNextPage) {
fetchNextPage ();
}
}, [inView, hasNextPage, fetchNextPage]);
return (
< div >
{data?.pages. map (( page ) => (
< div key = {page.nextCursor}>
{page.posts. map (( post ) => (
< div key = {post.id}>{post.title}</ div >
))}
</ div >
))}
< div ref = {ref}>
{isFetchingNextPage ? 'Loading more...' : hasNextPage ? 'Load More' : 'Nothing more' }
</ div >
</ div >
);
}
Ecosystem Integration Tips: Choose libraries based on project needs, bundle size matters,
prefer TypeScript-friendly libraries, check maintenance and community, test library compatibility, use
tree-shaking when available, consider migration path, read documentation thoroughly, start simple then add
complexity, benchmark performance in your app.
20. React Security and Best Practices
Attack Vector
Risk
Prevention
Example
User Input in JSX
XSS via script injection
React automatically escapes JSX expressions
{userInput} is safe - React escapes HTML
URL Parameters
JavaScript execution via href
Validate and sanitize URLs, block javascript: protocol
Use allowlist for protocols (http:, https:, mailto:)
HTML Attributes
Event handler injection
Never use user input directly in event handlers
Avoid: onClick={eval(userInput)}
innerHTML/outerHTML
Direct HTML injection
Use React rendering, avoid dangerouslySetInnerHTML
Sanitize with DOMPurify if HTML needed
CSS Injection
Style-based XSS
Validate style values, use CSS-in-JS safely
Avoid: style={{'{{'}}background: userInput}}
SVG Content
Script tags in SVG
Sanitize SVG content, validate sources
Use SVG as components, not raw strings
// ✅ SAFE - React automatically escapes
function UserComment ({ comment }) {
return < div >{comment.text}</ div >; // HTML entities escaped
}
// ✅ SAFE - Using textContent
function DisplayText ({ text }) {
const ref = useRef < HTMLDivElement >( null );
useEffect (() => {
if (ref.current) {
ref.current.textContent = text; // Safe, no HTML parsing
}
}, [text]);
return < div ref = {ref} />;
}
// ❌ UNSAFE - Direct href from user input
function UnsafeLink ({ url }) {
return < a href = {url}>Click</ a >; // Can be javascript:alert('XSS')
}
// ✅ SAFE - Validate URL protocol
function SafeLink ({ url }) {
const isValidUrl = ( url : string ) => {
try {
const parsed = new URL (url);
return [ 'http:' , 'https:' , 'mailto:' ]. includes (parsed.protocol);
} catch {
return false ;
}
};
if ( ! isValidUrl (url)) {
return < span >Invalid URL</ span >;
}
return < a href = {url} rel = "noopener noreferrer" >Click</ a >;
}
// Install DOMPurify for HTML sanitization
npm install dompurify
npm install -- save - dev @types / dompurify
import DOMPurify from 'dompurify' ;
// Sanitize HTML content
function SanitizedHtml ({ html } : { html : string }) {
const sanitized = DOMPurify. sanitize (html, {
ALLOWED_TAGS: [ 'b' , 'i' , 'em' , 'strong' , 'a' , 'p' , 'br' ],
ALLOWED_ATTR: [ 'href' , 'title' , 'target' ],
ALLOW_DATA_ATTR: false ,
});
return < div dangerouslySetInnerHTML = {{ __html: sanitized }} />;
}
// Sanitize user input before storing
function CommentForm () {
const [ comment , setComment ] = useState ( '' );
const handleSubmit = async ( e : FormEvent ) => {
e. preventDefault ();
// Sanitize before sending to API
const sanitized = DOMPurify. sanitize (comment, {
ALLOWED_TAGS: [ 'b' , 'i' , 'em' , 'strong' ],
ALLOWED_ATTR: [],
});
await saveComment (sanitized);
};
return (
< form onSubmit = {handleSubmit}>
< textarea
value = {comment}
onChange = {( e ) => setComment (e.target.value)}
maxLength = { 500 }
/>
< button type = "submit" >Post Comment</ button >
</ form >
);
}
20.2 dangerouslySetInnerHTML Safe Usage
Risk
Mitigation
Best Practice
Example
XSS Attacks
Always sanitize HTML before rendering
Use DOMPurify or similar library
Sanitize all user-generated content
Script Injection
Remove script tags and event handlers
Configure strict sanitization rules
Block <script>, onclick, onerror, etc.
Style Attacks
Limit allowed CSS properties
Whitelist safe CSS properties only
Avoid expression(), url(), import
Data Exfiltration
Block external resource loading
CSP headers, sanitize img/link src
Restrict to same-origin or trusted domains
iframe Injection
Block iframe tags unless necessary
Use sandbox attribute if needed
Restrict iframe capabilities
Form Hijacking
Block form tags in user content
Sanitize or block form elements
Prevent form submission hijacking
Example: Safe dangerouslySetInnerHTML usage
import DOMPurify from 'dompurify' ;
// ❌ NEVER DO THIS - Unsafe!
function UnsafeHtml ({ html }) {
return < div dangerouslySetInnerHTML = {{ __html: html }} />;
}
// ✅ SAFE - Sanitized HTML
function SafeHtml ({ html } : { html : string }) {
const sanitized = useMemo (() => {
return DOMPurify. sanitize (html, {
ALLOWED_TAGS: [
'h1' , 'h2' , 'h3' , 'p' , 'br' , 'strong' , 'em' , 'u' ,
'ul' , 'ol' , 'li' , 'a' , 'blockquote' , 'code' , 'pre'
],
ALLOWED_ATTR: {
'a' : [ 'href' , 'title' , 'target' , 'rel' ],
'img' : [ 'src' , 'alt' , 'title' , 'width' , 'height' ]
},
ALLOW_DATA_ATTR: false ,
FORBID_TAGS: [ 'script' , 'iframe' , 'object' , 'embed' , 'form' ],
FORBID_ATTR: [ 'onerror' , 'onload' , 'onclick' , 'onmouseover' ]
});
}, [html]);
return < div dangerouslySetInnerHTML = {{ __html: sanitized }} />;
}
// ✅ BETTER - Use React components instead
function MarkdownRenderer ({ markdown } : { markdown : string }) {
// Use a library like react-markdown
return < ReactMarkdown >{markdown}</ ReactMarkdown >;
}
// Safe HTML with hooks
function useSafeHtml ( html : string ) {
return useMemo (() => {
if ( ! html) return '' ;
return DOMPurify. sanitize (html, {
ALLOWED_TAGS: [ 'p' , 'br' , 'strong' , 'em' , 'a' ],
ALLOWED_ATTR: [ 'href' ],
KEEP_CONTENT: true ,
});
}, [html]);
}
function RichTextDisplay ({ content } : { content : string }) {
const safeHtml = useSafeHtml (content);
if ( ! content) {
return < p >No content available</ p >;
}
return (
< div
className = "rich-text-content"
dangerouslySetInnerHTML = {{ __html: safeHtml }}
/>
);
}
Warning: Never use dangerouslySetInnerHTML with unsanitized user input. Always validate HTML
source, sanitize with DOMPurify, use strict whitelist of tags/attributes, implement Content Security Policy,
prefer React components over raw HTML, audit third-party HTML carefully.
20.3 Props Validation and Runtime Type Checking
Approach
Tool
Features
Use Case
PropTypes
prop-types package
Runtime validation, custom validators, development warnings
JavaScript projects, simple validation
TypeScript
Built-in type system
Compile-time checking, inference, strict mode
Type-safe React apps, preferred approach
Zod Runtime
zod schemas
Runtime + compile-time, parsing, transformation
API responses, form validation, external data
io-ts
TypeScript runtime validation
Type guards, decoders, reporters
Complex validation, FP patterns
Yup
Schema validation
Object schemas, async validation, transforms
Form validation, data validation
Custom Validation
Manual checks
Full control, specific business logic
Complex validation requirements
Example: PropTypes validation
import PropTypes from 'prop-types' ;
function UserCard ({ user , onEdit , isActive }) {
return (
< div className = {isActive ? 'active' : '' }>
< h3 >{user.name}</ h3 >
< p >{user.email}</ p >
< button onClick = {onEdit}>Edit</ button >
</ div >
);
}
UserCard.propTypes = {
user: PropTypes. shape ({
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
email: PropTypes.string.isRequired,
avatar: PropTypes.string,
}).isRequired,
onEdit: PropTypes.func.isRequired,
isActive: PropTypes.bool,
};
UserCard.defaultProps = {
isActive: false ,
};
// Custom validators
function EmailValidator ( props , propName , componentName ) {
const email = props[propName];
if (email && ! / ^ [ \w - \. ] + @( [\w-] + \. ) + [\w-] {2,4}$ / . test (email)) {
return new Error (
`Invalid prop \` ${ propName } \` supplied to \` ${ componentName } \` . ` +
`Expected a valid email address.`
);
}
}
function ContactForm ({ email }) {
return < input type = "email" value = {email} />;
}
ContactForm.propTypes = {
email: EmailValidator
};
Example: TypeScript with runtime validation
import { z } from 'zod' ;
// Define schema
const UserSchema = z. object ({
id: z. number (). positive (),
name: z. string (). min ( 1 ). max ( 100 ),
email: z. string (). email (),
age: z. number (). min ( 0 ). max ( 150 ). optional (),
role: z. enum ([ 'admin' , 'user' , 'guest' ]),
});
type User = z . infer < typeof UserSchema>;
// Component with TypeScript
interface UserCardProps {
user : User ;
onEdit : ( user : User ) => void ;
isActive ?: boolean ;
}
function UserCard ({ user , onEdit , isActive = false } : UserCardProps ) {
return (
< div className = {isActive ? 'active' : '' }>
< h3 >{user.name}</ h3 >
< p >{user.email}</ p >
< button onClick = {() => onEdit (user)}>Edit</ button >
</ div >
);
}
// Validate API response
async function fetchUser ( id : number ) : Promise < User > {
const response = await fetch ( `/api/users/${ id }` );
const data = await response. json ();
// Runtime validation
try {
return UserSchema. parse (data);
} catch (error) {
if (error instanceof z . ZodError ) {
console. error ( 'Invalid user data:' , error.errors);
throw new Error ( 'Invalid user data from API' );
}
throw error;
}
}
// Safe parse without throwing
async function fetchUserSafe ( id : number ) {
const response = await fetch ( `/api/users/${ id }` );
const data = await response. json ();
const result = UserSchema. safeParse (data);
if ( ! result.success) {
return { error: result.error, data: null };
}
return { error: null , data: result.data };
}
20.4 Authentication and Authorization Patterns
Pattern
Implementation
Security Considerations
Use Case
JWT Tokens
Store in memory or httpOnly cookies
Never store in localStorage, use short expiry, refresh tokens
Stateless authentication, API access
Session Cookies
Server-side sessions, httpOnly secure cookies
CSRF protection, SameSite attribute, secure flag
Traditional web apps, server-side auth
OAuth 2.0 / OIDC
Third-party auth providers (Google, Auth0)
PKCE flow for SPAs, validate state parameter
Social login, enterprise SSO
Protected Routes
Conditional rendering, redirect logic
Server-side validation, check permissions
Role-based access control
API Security
Authorization headers, CORS configuration
Validate on server, don't trust client
API endpoint protection
MFA/2FA
Time-based OTP, SMS, authenticator apps
Backup codes, rate limiting, secure storage
Enhanced security for sensitive ops
Example: JWT authentication with refresh tokens
// AuthContext.tsx
interface AuthContextType {
user : User | null ;
login : ( email : string , password : string ) => Promise < void >;
logout : () => void ;
isAuthenticated : boolean ;
}
const AuthContext = createContext < AuthContextType | null >( null );
export function AuthProvider ({ children } : { children : ReactNode }) {
const [ user , setUser ] = useState < User | null >( null );
const [ accessToken , setAccessToken ] = useState < string | null >( null );
// Store access token in memory only
const login = async ( email : string , password : string ) => {
const response = await fetch ( '/api/auth/login' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
credentials: 'include' , // Include httpOnly refresh token cookie
body: JSON . stringify ({ email, password }),
});
if ( ! response.ok) throw new Error ( 'Login failed' );
const { user , accessToken } = await response. json ();
setUser (user);
setAccessToken (accessToken); // Store in memory
};
const logout = async () => {
await fetch ( '/api/auth/logout' , {
method: 'POST' ,
credentials: 'include' ,
});
setUser ( null );
setAccessToken ( null );
};
// Refresh access token
useEffect (() => {
const refreshToken = async () => {
try {
const response = await fetch ( '/api/auth/refresh' , {
method: 'POST' ,
credentials: 'include' , // Send httpOnly cookie
});
if (response.ok) {
const { user , accessToken } = await response. json ();
setUser (user);
setAccessToken (accessToken);
}
} catch (error) {
console. error ( 'Token refresh failed:' , error);
}
};
refreshToken ();
// Refresh before expiry (every 14 minutes for 15-minute tokens)
const interval = setInterval (refreshToken, 14 * 60 * 1000 );
return () => clearInterval (interval);
}, []);
return (
< AuthContext.Provider value = {{ user, login, logout, isAuthenticated: !! user }}>
{children}
</ AuthContext.Provider >
);
}
export const useAuth = () => {
const context = useContext (AuthContext);
if ( ! context) throw new Error ( 'useAuth must be used within AuthProvider' );
return context;
};
Example: Protected routes and role-based access
// ProtectedRoute.tsx
interface ProtectedRouteProps {
children : ReactNode ;
requiredRole ?: string [];
redirectTo ?: string ;
}
function ProtectedRoute ({
children ,
requiredRole ,
redirectTo = '/login'
} : ProtectedRouteProps ) {
const { user , isAuthenticated } = useAuth ();
const location = useLocation ();
if ( ! isAuthenticated) {
// Redirect to login, preserve intended destination
return < Navigate to = {redirectTo} state = {{ from: location }} replace />;
}
// Check role-based access
if (requiredRole && ! requiredRole. includes (user.role)) {
return < Navigate to = "/unauthorized" replace />;
}
return <>{children}</>;
}
// Usage in routes
function App () {
return (
< Routes >
< Route path = "/login" element = {< Login />} />
< Route path = "/unauthorized" element = {< Unauthorized />} />
< Route
path = "/dashboard"
element = {
< ProtectedRoute >
< Dashboard />
</ ProtectedRoute >
}
/>
< Route
path = "/admin"
element = {
< ProtectedRoute requiredRole = {[ 'admin' ]}>
< AdminPanel />
</ ProtectedRoute >
}
/>
</ Routes >
);
}
// Secure API requests
function useSecureApi () {
const { accessToken } = useAuth ();
const secureRequest = async ( url : string , options : RequestInit = {}) => {
const response = await fetch (url, {
... options,
headers: {
... options.headers,
'Authorization' : `Bearer ${ accessToken }` ,
'Content-Type' : 'application/json' ,
},
credentials: 'include' ,
});
if (response.status === 401 ) {
// Token expired, trigger logout
throw new Error ( 'Unauthorized' );
}
return response;
};
return { secureRequest };
}
20.5 Secure State Management Practices
Risk
Secure Practice
Implementation
Example
Sensitive Data in State
Never store passwords, credit cards, SSN in state
Use secure backend storage, tokenization
Store tokens server-side, use references only
localStorage Risks
Avoid storing auth tokens in localStorage
Use httpOnly cookies or memory storage
XSS can access localStorage but not httpOnly cookies
State Persistence
Encrypt sensitive persisted state
Use encryption libraries, secure key management
Encrypt before saving to storage
Redux DevTools
Disable in production or sanitize actions
Conditionally enable, filter sensitive data
Redact passwords, tokens from action logs
State Leakage
Clear sensitive state on logout
Reset stores, clear memory
Unmount components, clear caches
Cross-Tab State
Validate state across browser tabs
Use BroadcastChannel for sync, validate auth
Logout all tabs when one logs out
Example: Secure state management
// ❌ NEVER DO THIS - Insecure!
localStorage. setItem ( 'authToken' , token);
localStorage. setItem ( 'password' , password);
localStorage. setItem ( 'creditCard' , cardNumber);
// ✅ SECURE - In-memory only, httpOnly cookies for refresh
const AuthProvider = ({ children }) => {
// Access token in memory only
const [ accessToken , setAccessToken ] = useState < string | null >( null );
// Refresh token in httpOnly cookie (set by server)
// Cookie: refreshToken=xxx; HttpOnly; Secure; SameSite=Strict
return (
< AuthContext.Provider value = {{ accessToken }}>
{children}
</ AuthContext.Provider >
);
};
// Secure state persistence
import CryptoJS from 'crypto-js' ;
function useSecureStorage ( key : string ) {
const encryptionKey = process.env. REACT_APP_ENCRYPTION_KEY ;
const setSecureItem = ( value : any ) => {
const encrypted = CryptoJS. AES . encrypt (
JSON . stringify (value),
encryptionKey
). toString ();
localStorage. setItem (key, encrypted);
};
const getSecureItem = () => {
const encrypted = localStorage. getItem (key);
if ( ! encrypted) return null ;
try {
const decrypted = CryptoJS. AES . decrypt (encrypted, encryptionKey);
return JSON . parse (decrypted. toString (CryptoJS.enc.Utf8));
} catch {
return null ;
}
};
const removeSecureItem = () => {
localStorage. removeItem (key);
};
return { setSecureItem, getSecureItem, removeSecureItem };
}
// Redux DevTools - sanitize sensitive data
const store = configureStore ({
reducer: rootReducer,
devTools: process.env. NODE_ENV !== 'production' && {
actionSanitizer : ( action ) => ({
... action,
payload: action.type. includes ( 'PASSWORD' )
? '***REDACTED***'
: action.payload,
}),
stateSanitizer : ( state ) => ({
... state,
auth: {
... state.auth,
password: '***REDACTED***' ,
token: '***REDACTED***' ,
},
}),
},
});
// Clear state on logout
function useSecureLogout () {
const dispatch = useDispatch ();
const queryClient = useQueryClient ();
const logout = useCallback ( async () => {
// Clear Redux state
dispatch ({ type: 'RESET_STATE' });
// Clear React Query cache
queryClient. clear ();
// Clear any local storage (except user preferences)
const preferences = localStorage. getItem ( 'userPreferences' );
localStorage. clear ();
if (preferences) {
localStorage. setItem ( 'userPreferences' , preferences);
}
// Call logout API
await fetch ( '/api/auth/logout' , {
method: 'POST' ,
credentials: 'include' ,
});
// Redirect to login
window.location.href = '/login' ;
}, [dispatch, queryClient]);
return logout;
}
20.6 Content Security Policy with React Apps
CSP Directive
Purpose
React Configuration
Example Value
default-src
Fallback for other directives
Set restrictive default, override as needed
'self'
script-src
Control script execution
Use nonce or hash for inline scripts
'self' 'nonce-xyz123'
style-src
Control stylesheet loading
Use nonce for CSS-in-JS libraries
'self' 'unsafe-inline' (use nonce instead)
img-src
Control image sources
Whitelist CDNs and data URIs if needed
'self' data: https://cdn.example.com
connect-src
Control fetch/XHR destinations
Whitelist API endpoints
'self' https://api.example.com
frame-ancestors
Prevent clickjacking
Control iframe embedding
'none' or 'self'
upgrade-insecure-requests
Auto upgrade HTTP to HTTPS
Enable for production
No value needed (directive only)
// Next.js - next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy' ,
value: [
"default-src 'self'" ,
"script-src 'self' 'unsafe-eval' 'unsafe-inline'" , // Adjust for production
"style-src 'self' 'unsafe-inline'" ,
"img-src 'self' data: https:" ,
"font-src 'self' data:" ,
"connect-src 'self' https://api.example.com" ,
"frame-ancestors 'none'" ,
"base-uri 'self'" ,
"form-action 'self'" ,
"upgrade-insecure-requests" ,
]. join ( '; ' ),
},
{
key: 'X-Frame-Options' ,
value: 'DENY' ,
},
{
key: 'X-Content-Type-Options' ,
value: 'nosniff' ,
},
{
key: 'Referrer-Policy' ,
value: 'strict-origin-when-cross-origin' ,
},
{
key: 'Permissions-Policy' ,
value: 'camera=(), microphone=(), geolocation=()' ,
},
];
module . exports = {
async headers () {
return [
{
source: '/:path*' ,
headers: securityHeaders,
},
];
},
};
// Express.js backend
import helmet from 'helmet' ;
app. use (
helmet. contentSecurityPolicy ({
directives: {
defaultSrc: [ "'self'" ],
scriptSrc: [ "'self'" , "'unsafe-inline'" ], // Use nonce in production
styleSrc: [ "'self'" , "'unsafe-inline'" ],
imgSrc: [ "'self'" , "data:" , "https:" ],
connectSrc: [ "'self'" , "https://api.example.com" ],
fontSrc: [ "'self'" , "data:" ],
objectSrc: [ "'none'" ],
mediaSrc: [ "'self'" ],
frameSrc: [ "'none'" ],
frameAncestors: [ "'none'" ],
baseUri: [ "'self'" ],
formAction: [ "'self'" ],
upgradeInsecureRequests: [],
},
})
);
Example: Using CSP nonces with React
// Server-side (Express with React SSR)
import crypto from 'crypto' ;
import { renderToString } from 'react-dom/server' ;
app. get ( '*' , ( req , res ) => {
// Generate nonce for this request
const nonce = crypto. randomBytes ( 16 ). toString ( 'base64' );
// Set CSP header with nonce
res. setHeader (
'Content-Security-Policy' ,
`script-src 'self' 'nonce-${ nonce }'; style-src 'self' 'nonce-${ nonce }'`
);
// Render app with nonce
const html = renderToString (< App nonce = {nonce} />);
res. send ( `
<!DOCTYPE html>
<html>
<head>
<style nonce="${ nonce }">
/* Critical CSS */
</style>
</head>
<body>
<div id="root">${ html }</div>
<script nonce="${ nonce }" src="/static/bundle.js"></script>
</body>
</html>
` );
});
// React component using nonce
function App ({ nonce }) {
return (
< HelmetProvider >
< Helmet >
< script nonce = {nonce}>
{ `window.__INITIAL_STATE__ = ${ JSON . stringify ( initialState ) };` }
</ script >
</ Helmet >
< YourApp />
</ HelmetProvider >
);
}
// Strict CSP for production
const productionCSP = {
defaultSrc: [ "'none'" ],
scriptSrc: [ "'self'" ],
styleSrc: [ "'self'" ],
imgSrc: [ "'self'" , "data:" , "https://trusted-cdn.com" ],
fontSrc: [ "'self'" ],
connectSrc: [ "'self'" , "https://api.example.com" ],
frameSrc: [ "'none'" ],
objectSrc: [ "'none'" ],
baseUri: [ "'self'" ],
formAction: [ "'self'" ],
frameAncestors: [ "'none'" ],
upgradeInsecureRequests: [],
};
CSP Best Practices: Start with strict policy, test thoroughly before production, avoid
'unsafe-inline' and 'unsafe-eval', use nonces or hashes for inline scripts, whitelist specific domains not
wildcards, implement CSP reporting endpoint, monitor violations, update policy as app evolves, test in
report-only mode first.
21. React TypeScript Integration
21.1 Component Props TypeScript Interfaces
Pattern
Syntax
Use Case
Example
Basic Interface
interface Props { ... }
Define component prop types
Simple props with required/optional fields
Type Alias
type Props = { ... }
Alternative to interface, unions, intersections
When needing union types or primitives
Optional Props
prop?: string
Props that may not be provided
Default values, conditional rendering
Default Props
Destructure with defaults
Provide fallback values
{ size = 'medium' }
Children Prop
children: React.ReactNode
Type children elements
Wrapper components, layouts
Generic Props
interface Props<T> { ... }
Type-safe generic components
Lists, tables, data displays
Discriminated Unions
type Props = A | B
Variant props based on type field
Button variants, conditional props
Example: Component props interfaces
// Basic interface
interface ButtonProps {
label : string ;
onClick : () => void ;
disabled ?: boolean ;
variant ?: 'primary' | 'secondary' | 'danger' ;
}
function Button ({ label , onClick , disabled = false , variant = 'primary' } : ButtonProps ) {
return (
< button
onClick = {onClick}
disabled = {disabled}
className = { `btn btn-${ variant }` }
>
{label}
</ button >
);
}
// Children prop
interface CardProps {
title : string ;
children : React . ReactNode ;
footer ?: React . ReactNode ;
}
function Card ({ title , children , footer } : CardProps ) {
return (
< div className = "card" >
< h2 >{title}</ h2 >
< div className = "card-body" >{children}</ div >
{footer && < div className = "card-footer" >{footer}</ div >}
</ div >
);
}
// Generic props
interface ListProps < T > {
items : T [];
renderItem : ( item : T , index : number ) => React . ReactNode ;
keyExtractor : ( item : T ) => string | number ;
}
function List < T >({ items , renderItem , keyExtractor } : ListProps < T >) {
return (
< ul >
{items. map (( item , index ) => (
< li key = { keyExtractor (item)}>
{ renderItem (item, index)}
</ li >
))}
</ ul >
);
}
// Usage with type inference
< List
items = {users}
renderItem = {( user ) => < span >{user.name}</ span >}
keyExtractor = {( user ) => user.id}
/>
Example: Discriminated unions for variant props
// Button with discriminated union
type BaseButtonProps = {
label : string ;
disabled ?: boolean ;
};
type PrimaryButtonProps = BaseButtonProps & {
variant : 'primary' ;
color : 'blue' | 'green' | 'red' ;
};
type SecondaryButtonProps = BaseButtonProps & {
variant : 'secondary' ;
outlined : boolean ;
};
type LinkButtonProps = BaseButtonProps & {
variant : 'link' ;
href : string ;
target ?: '_blank' | '_self' ;
};
type ButtonProps = PrimaryButtonProps | SecondaryButtonProps | LinkButtonProps ;
function Button ( props : ButtonProps ) {
const { variant , label , disabled } = props;
if (variant === 'primary' ) {
// TypeScript knows props.color exists
return < button className = { `btn-${ props . color }` }>{label}</ button >;
}
if (variant === 'secondary' ) {
// TypeScript knows props.outlined exists
return < button className = {props.outlined ? 'outlined' : '' }>{label}</ button >;
}
// TypeScript knows props.href exists
return < a href = {props.href} target = {props.target}>{label}</ a >;
}
// Complex discriminated union
type FormFieldProps =
| { type : 'text' ; maxLength ?: number ; pattern ?: string }
| { type : 'number' ; min ?: number ; max ?: number ; step ?: number }
| { type : 'select' ; options : Array <{ label : string ; value : string }> }
| { type : 'checkbox' ; checked : boolean };
function FormField ( props : FormFieldProps & { name : string ; label : string }) {
switch (props.type) {
case 'text' :
return < input type = "text" maxLength = {props.maxLength} pattern = {props.pattern} />;
case 'number' :
return < input type = "number" min = {props.min} max = {props.max} step = {props.step} />;
case 'select' :
return (
< select >
{props.options. map ( opt => < option value = {opt.value}>{opt.label}</ option >)}
</ select >
);
case 'checkbox' :
return < input type = "checkbox" checked = {props.checked} />;
}
}
21.2 Hook Type Definitions and Generic Hooks
Hook
Type Syntax
Type Inference
Example
useState
useState<Type>(initial)
Auto-infers from initial value
const [count, setCount] = useState(0)
useRef
useRef<HTMLElement | null>(null)
Must specify element type
const ref = useRef<HTMLDivElement>(null)
useReducer
useReducer<Reducer<State, Action>>
Type state and action
Typed reducer with discriminated unions
useContext
useContext<ContextType>(Context)
Infers from context creation
Type-safe context consumption
useMemo
useMemo<ReturnType>(() => ...)
Auto-infers return type
Type-safe memoization
useCallback
useCallback<FunctionType>(...)
Infers function signature
Type-safe callbacks
Custom Hooks
function useHook<T>(): ReturnType
Explicit return type
Generic reusable hooks
Example: Typed React hooks
// useState with explicit type
const [ user , setUser ] = useState < User | null >( null );
const [ count , setCount ] = useState ( 0 ); // inferred as number
// useState with complex types
interface FormState {
email : string ;
password : string ;
errors : Record < string , string >;
}
const [ form , setForm ] = useState < FormState >({
email: '' ,
password: '' ,
errors: {},
});
// useRef with DOM elements
const inputRef = useRef < HTMLInputElement >( null );
const divRef = useRef < HTMLDivElement >( null );
useEffect (() => {
if (inputRef.current) {
inputRef.current. focus (); // TypeScript knows .focus() exists
}
}, []);
// useRef for mutable values
const intervalRef = useRef < NodeJS . Timeout | null >( null );
useEffect (() => {
intervalRef.current = setInterval (() => {
console. log ( 'tick' );
}, 1000 );
return () => {
if (intervalRef.current) {
clearInterval (intervalRef.current);
}
};
}, []);
// useReducer with typed actions
type State = { count : number ; lastAction : string };
type Action =
| { type : 'increment' ; payload ?: number }
| { type : 'decrement' ; payload ?: number }
| { type : 'reset' };
function reducer ( state : State , action : Action ) : State {
switch (action.type) {
case 'increment' :
return {
count: state.count + (action.payload ?? 1 ),
lastAction: 'increment'
};
case 'decrement' :
return {
count: state.count - (action.payload ?? 1 ),
lastAction: 'decrement'
};
case 'reset' :
return { count: 0 , lastAction: 'reset' };
default :
return state;
}
}
const [ state , dispatch ] = useReducer (reducer, { count: 0 , lastAction: 'init' });
// TypeScript validates action types
dispatch ({ type: 'increment' , payload: 5 }); // ✅
dispatch ({ type: 'invalid' }); // ❌ Error
Example: Generic custom hooks
// Generic fetch hook
function useFetch < T >( url : string ) {
const [ data , setData ] = useState < T | null >( null );
const [ loading , setLoading ] = useState ( true );
const [ error , setError ] = useState < Error | null >( null );
useEffect (() => {
let cancelled = false ;
async function fetchData () {
try {
const response = await fetch (url);
const json = await response. json ();
if ( ! cancelled) {
setData (json as T );
setLoading ( false );
}
} catch (err) {
if ( ! cancelled) {
setError (err instanceof Error ? err : new Error ( 'Unknown error' ));
setLoading ( false );
}
}
}
fetchData ();
return () => {
cancelled = true ;
};
}, [url]);
return { data, loading, error };
}
// Usage with type inference
interface User {
id : number ;
name : string ;
email : string ;
}
function UserProfile ({ userId } : { userId : number }) {
const { data : user , loading , error } = useFetch < User >( `/api/users/${ userId }` );
if (loading) return < div >Loading...</ div >;
if (error) return < div >Error: {error.message}</ div >;
if ( ! user) return < div >No user found</ div >;
// TypeScript knows user has id, name, email
return < div >{user.name} - {user.email}</ div >;
}
// Generic array hook with constraints
function useArray < T >( initialValue : T [] = []) {
const [ array , setArray ] = useState < T []>(initialValue);
const push = useCallback (( item : T ) => {
setArray ( prev => [ ... prev, item]);
}, []);
const remove = useCallback (( index : number ) => {
setArray ( prev => prev. filter (( _ , i ) => i !== index));
}, []);
const update = useCallback (( index : number , item : T ) => {
setArray ( prev => prev. map (( val , i ) => i === index ? item : val));
}, []);
const clear = useCallback (() => {
setArray ([]);
}, []);
return { array, set: setArray, push, remove, update, clear };
}
// Usage
const { array : todos , push , remove } = useArray < Todo >([]);
// Generic storage hook
function useLocalStorage < T >( key : string , initialValue : T ) {
const [ storedValue , setStoredValue ] = useState < T >(() => {
try {
const item = window.localStorage. getItem (key);
return item ? JSON . parse (item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = useCallback (( value : T | (( val : T ) => T )) => {
try {
const valueToStore = value instanceof Function ? value (storedValue) : value;
setStoredValue (valueToStore);
window.localStorage. setItem (key, JSON . stringify (valueToStore));
} catch (error) {
console. error (error);
}
}, [key, storedValue]);
return [storedValue, setValue] as const ;
}
21.3 Event Handler Type Safety
Event Type
React Type
Native Type
Common Usage
Click
React.MouseEvent<HTMLElement>
MouseEvent
Button clicks, div clicks
Change
React.ChangeEvent<HTMLInputElement>
Event
Input, textarea, select changes
Submit
React.FormEvent<HTMLFormElement>
Event
Form submissions
Focus
React.FocusEvent<HTMLElement>
FocusEvent
Input focus/blur
Keyboard
React.KeyboardEvent<HTMLElement>
KeyboardEvent
Key press, key down, key up
Drag
React.DragEvent<HTMLElement>
DragEvent
Drag and drop interactions
Example: Type-safe event handlers
// Click events
function handleClick ( event : React . MouseEvent < HTMLButtonElement >) {
event. preventDefault ();
console. log ( 'Button clicked' , event.currentTarget.name);
}
// Different element types
function handleDivClick ( event : React . MouseEvent < HTMLDivElement >) {
console. log ( 'Div clicked at' , event.clientX, event.clientY);
}
// Input change events
function handleChange ( event : React . ChangeEvent < HTMLInputElement >) {
const value = event.target.value;
const name = event.target.name;
console. log ( `${ name }: ${ value }` );
}
// Select change events
function handleSelectChange ( event : React . ChangeEvent < HTMLSelectElement >) {
const selectedValue = event.target.value;
const selectedIndex = event.target.selectedIndex;
console. log ( `Selected: ${ selectedValue } at index ${ selectedIndex }` );
}
// Form submission
function handleSubmit ( event : React . FormEvent < HTMLFormElement >) {
event. preventDefault ();
const formData = new FormData (event.currentTarget);
const data = Object. fromEntries (formData);
console. log ( 'Form data:' , data);
}
// Keyboard events
function handleKeyDown ( event : React . KeyboardEvent < HTMLInputElement >) {
if (event.key === 'Enter' && ! event.shiftKey) {
event. preventDefault ();
console. log ( 'Enter pressed' );
}
// Check modifiers
if (event.ctrlKey && event.key === 's' ) {
event. preventDefault ();
console. log ( 'Ctrl+S pressed' );
}
}
// Complete form component
interface FormData {
email : string ;
password : string ;
}
function LoginForm () {
const [ formData , setFormData ] = useState < FormData >({
email: '' ,
password: '' ,
});
const handleInputChange = ( event : React . ChangeEvent < HTMLInputElement >) => {
const { name , value } = event.target;
setFormData ( prev => ({ ... prev, [name]: value }));
};
const handleFormSubmit = ( event : React . FormEvent < HTMLFormElement >) => {
event. preventDefault ();
console. log ( 'Submitting:' , formData);
};
return (
< form onSubmit = {handleFormSubmit}>
< input
type = "email"
name = "email"
value = {formData.email}
onChange = {handleInputChange}
/>
< input
type = "password"
name = "password"
value = {formData.password}
onChange = {handleInputChange}
/>
< button type = "submit" >Login</ button >
</ form >
);
}
Example: Generic event handler types
// Reusable event handler types
type ClickHandler = React . MouseEvent < HTMLButtonElement >;
type InputChangeHandler = React . ChangeEvent < HTMLInputElement >;
type FormSubmitHandler = React . FormEvent < HTMLFormElement >;
interface ButtonProps {
onClick : ( event : ClickHandler ) => void ;
label : string ;
}
function Button ({ onClick , label } : ButtonProps ) {
return < button onClick = {onClick}>{label}</ button >;
}
// Generic handler wrapper
type ElementClickHandler < T extends HTMLElement > = (
event : React . MouseEvent < T >
) => void ;
interface ClickableProps < T extends HTMLElement > {
onClick : ElementClickHandler < T >;
children : React . ReactNode ;
}
// Event handler with custom parameters
interface SearchInputProps {
onSearch : ( query : string ) => void ;
onClear : () => void ;
}
function SearchInput ({ onSearch , onClear } : SearchInputProps ) {
const [ query , setQuery ] = useState ( '' );
const handleChange : InputChangeHandler = ( event ) => {
setQuery (event.target.value);
};
const handleSubmit : FormSubmitHandler = ( event ) => {
event. preventDefault ();
onSearch (query); // Call with typed parameter
};
const handleClear : ClickHandler = ( event ) => {
event. preventDefault ();
setQuery ( '' );
onClear ();
};
return (
< form onSubmit = {handleSubmit}>
< input value = {query} onChange = {handleChange} />
< button type = "submit" >Search</ button >
< button type = "button" onClick = {handleClear}>Clear</ button >
</ form >
);
}
21.4 Context API TypeScript Patterns
Pattern
Implementation
Benefits
Use Case
Typed Context
createContext<Type | null>(null)
Type safety for context value
All context implementations
Context with Hook
Custom hook that throws if outside provider
Guaranteed non-null context, better DX
Enforcing provider usage
Generic Context
createContext<T> with generic
Reusable context pattern
Generic state containers
Context with Actions
Separate state and dispatch
Type-safe actions, better organization
Complex state management
Multiple Contexts
Compose multiple providers
Separation of concerns
Large apps with different domains
Example: Type-safe context with custom hook
// Define context type
interface User {
id : string ;
name : string ;
email : string ;
role : 'admin' | 'user' ;
}
interface AuthContextType {
user : User | null ;
login : ( email : string , password : string ) => Promise < void >;
logout : () => void ;
isAuthenticated : boolean ;
}
// Create context with null default
const AuthContext = createContext < AuthContextType | null >( null );
// Provider component
export function AuthProvider ({ children } : { children : React . ReactNode }) {
const [ user , setUser ] = useState < User | null >( null );
const login = async ( email : string , password : string ) => {
// API call
const response = await fetch ( '/api/login' , {
method: 'POST' ,
body: JSON . stringify ({ email, password }),
});
const userData = await response. json ();
setUser (userData);
};
const logout = () => {
setUser ( null );
};
const value : AuthContextType = {
user,
login,
logout,
isAuthenticated: user !== null ,
};
return < AuthContext.Provider value = {value}>{children}</ AuthContext.Provider >;
}
// Custom hook with type guard
export function useAuth () : AuthContextType {
const context = useContext (AuthContext);
if ( ! context) {
throw new Error ( 'useAuth must be used within AuthProvider' );
}
return context;
}
// Usage - TypeScript knows context is never null
function Profile () {
const { user , logout } = useAuth (); // No null check needed
return (
< div >
< h1 >{user?.name}</ h1 >
< button onClick = {logout}>Logout</ button >
</ div >
);
}
Example: Context with reducer pattern
// State and actions
interface Todo {
id : string ;
text : string ;
completed : boolean ;
}
interface TodoState {
todos : Todo [];
filter : 'all' | 'active' | 'completed' ;
}
type TodoAction =
| { type : 'ADD_TODO' ; text : string }
| { type : 'TOGGLE_TODO' ; id : string }
| { type : 'DELETE_TODO' ; id : string }
| { type : 'SET_FILTER' ; filter : TodoState [ 'filter' ] };
// Reducer
function todoReducer ( state : TodoState , action : TodoAction ) : TodoState {
switch (action.type) {
case 'ADD_TODO' :
return {
... state,
todos: [
... state.todos,
{ id: Date. now (). toString (), text: action.text, completed: false },
],
};
case 'TOGGLE_TODO' :
return {
... state,
todos: state.todos. map ( todo =>
todo.id === action.id ? { ... todo, completed: ! todo.completed } : todo
),
};
case 'DELETE_TODO' :
return {
... state,
todos: state.todos. filter ( todo => todo.id !== action.id),
};
case 'SET_FILTER' :
return { ... state, filter: action.filter };
default :
return state;
}
}
// Context types
interface TodoContextType {
state : TodoState ;
dispatch : React . Dispatch < TodoAction >;
}
const TodoContext = createContext < TodoContextType | null >( null );
// Provider
export function TodoProvider ({ children } : { children : React . ReactNode }) {
const [ state , dispatch ] = useReducer (todoReducer, {
todos: [],
filter: 'all' ,
});
return (
< TodoContext.Provider value = {{ state, dispatch }}>
{children}
</ TodoContext.Provider >
);
}
// Custom hooks
export function useTodos () {
const context = useContext (TodoContext);
if ( ! context) throw new Error ( 'useTodos must be used within TodoProvider' );
return context;
}
// Helper hooks
export function useTodoActions () {
const { dispatch } = useTodos ();
return {
addTodo : ( text : string ) => dispatch ({ type: 'ADD_TODO' , text }),
toggleTodo : ( id : string ) => dispatch ({ type: 'TOGGLE_TODO' , id }),
deleteTodo : ( id : string ) => dispatch ({ type: 'DELETE_TODO' , id }),
setFilter : ( filter : TodoState [ 'filter' ]) => dispatch ({ type: 'SET_FILTER' , filter }),
};
}
// Usage
function TodoList () {
const { state } = useTodos ();
const { toggleTodo , deleteTodo } = useTodoActions ();
return (
< ul >
{state.todos. map ( todo => (
< li key = {todo.id}>
< input
type = "checkbox"
checked = {todo.completed}
onChange = {() => toggleTodo (todo.id)}
/>
{todo.text}
< button onClick = {() => deleteTodo (todo.id)}>Delete</ button >
</ li >
))}
</ ul >
);
}
21.5 Custom Hook Type Safety and Inference
Concept
Pattern
Benefit
Example
Return Type Inference
Let TypeScript infer return type
Less verbose, auto-updates
Hook without explicit return type
Explicit Return Type
Define return type explicitly
Better documentation, API contracts
Public library hooks
Tuple Return
return [value, setter] as const
Array destructuring with types
useState-like hooks
Generic Hooks
function useHook<T>()
Reusable with different types
Generic data fetching, storage
Conditional Types
Return type based on parameters
Smart type inference
Optional parameters affecting return
Type Guards
Runtime checks with type narrowing
Type safety with validation
Parsing external data
Example: Custom hook with type inference
// Inferred return type
function useCounter ( initialValue = 0 ) {
const [ count , setCount ] = useState (initialValue);
const increment = useCallback (() => setCount ( c => c + 1 ), []);
const decrement = useCallback (() => setCount ( c => c - 1 ), []);
const reset = useCallback (() => setCount (initialValue), [initialValue]);
// Return type inferred as { count: number; increment: () => void; ... }
return { count, increment, decrement, reset };
}
// Tuple return with const assertion
function useToggle ( initialValue = false ) {
const [ value , setValue ] = useState (initialValue);
const toggle = useCallback (() => setValue ( v => ! v), []);
// 'as const' makes it a tuple, not array
return [value, toggle] as const ;
}
// Usage with proper types
const [ isOpen , toggleOpen ] = useToggle (); // isOpen: boolean, toggleOpen: () => void
// Generic hook with constraints
function useAsync < T , E = Error >(
asyncFunction : () => Promise < T >,
immediate = true
) {
const [ status , setStatus ] = useState < 'idle' | 'pending' | 'success' | 'error' >( 'idle' );
const [ data , setData ] = useState < T | null >( null );
const [ error , setError ] = useState < E | null >( null );
const execute = useCallback ( async () => {
setStatus ( 'pending' );
setData ( null );
setError ( null );
try {
const response = await asyncFunction ();
setData (response);
setStatus ( 'success' );
return response;
} catch (err) {
setError (err as E );
setStatus ( 'error' );
throw err;
}
}, [asyncFunction]);
useEffect (() => {
if (immediate) {
execute ();
}
}, [execute, immediate]);
return { execute, status, data, error };
}
// Usage with type inference
interface User {
id : number ;
name : string ;
}
function UserProfile () {
const { data : user , status } = useAsync < User >(
() => fetch ( '/api/user' ). then ( r => r. json ()),
true
);
// TypeScript knows user is User | null
if (status === 'success' && user) {
return < div >{user.name}</ div >;
}
return < div >Loading...</ div >;
}
Example: Advanced generic hooks
// Generic form hook
interface UseFormOptions < T > {
initialValues : T ;
onSubmit : ( values : T ) => void | Promise < void >;
validate ?: ( values : T ) => Partial < Record < keyof T , string >>;
}
function useForm < T extends Record < string , any >>({
initialValues ,
onSubmit ,
validate ,
} : UseFormOptions < T >) {
const [ values , setValues ] = useState < T >(initialValues);
const [ errors , setErrors ] = useState < Partial < Record < keyof T , string >>>({});
const [ isSubmitting , setIsSubmitting ] = useState ( false );
const handleChange = useCallback (( name : keyof T , value : any ) => {
setValues ( prev => ({ ... prev, [name]: value }));
setErrors ( prev => ({ ... prev, [name]: undefined }));
}, []);
const handleSubmit = useCallback ( async ( event ?: React . FormEvent ) => {
event?. preventDefault ();
if (validate) {
const validationErrors = validate (values);
if (Object. keys (validationErrors). length > 0 ) {
setErrors (validationErrors);
return ;
}
}
setIsSubmitting ( true );
try {
await onSubmit (values);
} finally {
setIsSubmitting ( false );
}
}, [values, validate, onSubmit]);
const reset = useCallback (() => {
setValues (initialValues);
setErrors ({});
}, [initialValues]);
return { values, errors, isSubmitting, handleChange, handleSubmit, reset };
}
// Usage with type inference
interface LoginForm {
email : string ;
password : string ;
}
function Login () {
const { values , errors , handleChange , handleSubmit } = useForm < LoginForm >({
initialValues: { email: '' , password: '' },
onSubmit : async ( values ) => {
await login (values.email, values.password);
},
validate : ( values ) => {
const errors : Partial < Record < keyof LoginForm , string >> = {};
if ( ! values.email) errors.email = 'Required' ;
if ( ! values.password) errors.password = 'Required' ;
return errors;
},
});
// TypeScript knows values.email and values.password exist
return (
< form onSubmit = {handleSubmit}>
< input
value = {values.email}
onChange = {( e ) => handleChange ( 'email' , e.target.value)}
/>
{errors.email && < span >{errors.email}</ span >}
< input
type = "password"
value = {values.password}
onChange = {( e ) => handleChange ( 'password' , e.target.value)}
/>
{errors.password && < span >{errors.password}</ span >}
< button type = "submit" >Login</ button >
</ form >
);
}
21.6 React.FC vs Function Component Types
Approach
Syntax
Pros
Cons
React.FC
const Comp: React.FC<Props> = ...
Includes children type, shorter syntax
Implicit children (not always wanted), verbose
Function Declaration
function Comp(props: Props) {...}
Explicit, clear, standard TypeScript
Need to define children explicitly
Arrow Function
const Comp = (props: Props) => ...
Concise, explicit types
No hoisting, slightly more verbose
With Generics
function Comp<T>(props: Props<T>) {...}
Type-safe generic components
More complex, requires understanding generics
Example: React.FC vs function declaration
// ❌ React.FC (less recommended now)
interface ButtonProps {
label : string ;
onClick : () => void ;
}
const Button : React . FC < ButtonProps > = ({ label , onClick , children }) => {
// children is implicitly included (might not be wanted)
return < button onClick = {onClick}>{label}{children}</ button >;
};
// ✅ Function declaration (recommended)
interface CardProps {
title : string ;
children : React . ReactNode ; // Explicit children
}
function Card ({ title , children } : CardProps ) {
return (
< div className = "card" >
< h2 >{title}</ h2 >
{children}
</ div >
);
}
// ✅ Arrow function (also good)
interface AlertProps {
message : string ;
type : 'info' | 'warning' | 'error' ;
}
const Alert = ({ message , type } : AlertProps ) => {
return < div className = { `alert alert-${ type }` }>{message}</ div >;
};
// ✅ Function with explicit return type
function UserCard ({ user } : { user : User }) : JSX . Element {
return (
< div >
< h3 >{user.name}</ h3 >
< p >{user.email}</ p >
</ div >
);
}
// Generic component - function declaration
function List < T >({ items , renderItem } : {
items : T [];
renderItem : ( item : T ) => React . ReactNode ;
}) {
return (
< ul >
{items. map (( item , index ) => (
< li key = {index}>{ renderItem (item)}</ li >
))}
</ ul >
);
}
// Component with default props (function declaration)
interface BadgeProps {
text : string ;
color ?: 'red' | 'blue' | 'green' ;
size ?: 'small' | 'medium' | 'large' ;
}
function Badge ({ text , color = 'blue' , size = 'medium' } : BadgeProps ) {
return < span className = { `badge badge-${ color } badge-${ size }` }>{text}</ span >;
}
Example: Component type patterns
// Polymorphic component (advanced)
type AsProp < C extends React . ElementType > = {
as ?: C ;
};
type PropsToOmit < C extends React . ElementType , P > = keyof ( AsProp < C > & P );
type PolymorphicComponentProp <
C extends React . ElementType ,
Props = {}
> = React . PropsWithChildren < Props & AsProp < C >> &
Omit < React . ComponentPropsWithoutRef < C >, PropsToOmit < C , Props >>;
type PolymorphicComponentPropWithRef <
C extends React . ElementType ,
Props = {}
> = PolymorphicComponentProp < C , Props > & { ref ?: PolymorphicRef < C > };
type PolymorphicRef < C extends React . ElementType > =
React . ComponentPropsWithRef < C >[ 'ref' ];
// Usage
interface TextProps {
color ?: 'primary' | 'secondary' ;
}
function Text < C extends React . ElementType = 'span' >({
as ,
color = 'primary' ,
children ,
... props
} : PolymorphicComponentPropWithRef < C , TextProps >) {
const Component = as || 'span' ;
return (
< Component className = { `text-${ color }` } { ... props}>
{children}
</ Component >
);
}
// Can be used as any element
< Text >Default span</ Text >
< Text as = "h1" >Heading</ Text >
< Text as = "a" href = "/link" >Link</ Text > // href is type-safe!
// Component with display name
const MyComponent = ({ name } : { name : string }) => {
return < div >{name}</ div >;
};
MyComponent.displayName = 'MyComponent' ; // For React DevTools
// Component with static methods
interface TabsComponent {
({ children } : { children : React . ReactNode }) : JSX . Element ;
Panel : ({ children } : { children : React . ReactNode }) => JSX . Element ;
}
const Tabs : TabsComponent = ({ children }) => {
return < div className = "tabs" >{children}</ div >;
};
Tabs. Panel = ({ children }) => {
return < div className = "tab-panel" >{children}</ div >;
};
// Usage: <Tabs><Tabs.Panel>Content</Tabs.Panel></Tabs>
TypeScript Best Practices: Prefer function declarations over React.FC, explicitly define
children prop when needed, use generics for reusable components, leverage type inference when possible, use
discriminated unions for variant props, add explicit return types for public APIs, use const assertions for
tuple returns, validate external data with runtime checks.
Feature
API
Metrics Captured
Use Case
Profiler Component
<Profiler id="..." onRender={...}>
Render time, phase, actual duration
Component-level performance tracking
onRender Callback
onRender(id, phase, actualDuration, ...)
All render metrics per component tree
Collect performance data programmatically
Interaction Tracing
DevTools Profiler with interactions
User interactions causing renders
Debug performance bottlenecks
Commit Phase Timing
baseDuration vs actualDuration
Optimization impact measurement
Measure memo/callback effectiveness
Custom Metrics
Performance API integration
Custom timing, user timing marks
Business-specific metrics
Production Profiling
React with profiling build
Real user performance data
Production performance monitoring
Example: React Profiler API usage
import { Profiler, ProfilerOnRenderCallback } from 'react' ;
// Profiler callback
const onRenderCallback : ProfilerOnRenderCallback = (
id , // Profiler id
phase , // "mount" or "update"
actualDuration , // Time spent rendering
baseDuration , // Estimated time without memoization
startTime , // When React began rendering
commitTime , // When React committed the update
interactions // Set of interactions for this update
) => {
console. log ( `Profiler [${ id }] - ${ phase }` );
console. log ( `Actual duration: ${ actualDuration }ms` );
console. log ( `Base duration: ${ baseDuration }ms` );
console. log ( `Time saved: ${ baseDuration - actualDuration }ms` );
// Send to analytics
if (actualDuration > 16 ) { // More than one frame
sendToAnalytics ({
component: id,
phase,
duration: actualDuration,
timestamp: commitTime,
});
}
};
// Wrap component tree with Profiler
function App () {
return (
< Profiler id = "App" onRender = {onRenderCallback}>
< Header />
< Profiler id = "Navigation" onRender = {onRenderCallback}>
< Navigation />
</ Profiler >
< Profiler id = "Content" onRender = {onRenderCallback}>
< MainContent />
</ Profiler >
< Footer />
</ Profiler >
);
}
// Custom performance tracking hook
function usePerformanceTracking ( componentName : string ) {
useEffect (() => {
const startMark = `${ componentName }-start` ;
const endMark = `${ componentName }-end` ;
const measureName = `${ componentName }-render` ;
performance. mark (startMark);
return () => {
performance. mark (endMark);
performance. measure (measureName, startMark, endMark);
const measure = performance. getEntriesByName (measureName)[ 0 ];
console. log ( `${ componentName } render time: ${ measure . duration }ms` );
// Clean up
performance. clearMarks (startMark);
performance. clearMarks (endMark);
performance. clearMeasures (measureName);
};
});
}
// Usage in component
function ExpensiveComponent () {
usePerformanceTracking ( 'ExpensiveComponent' );
// Component logic
return < div >...</ div >;
}
// Performance monitoring service
class PerformanceMonitor {
private metrics : Map < string , number []> = new Map ();
recordMetric ( name : string , duration : number ) {
if ( ! this .metrics. has (name)) {
this .metrics. set (name, []);
}
this .metrics. get (name) ! . push (duration);
}
getStats ( name : string ) {
const durations = this .metrics. get (name) || [];
if (durations. length === 0 ) return null ;
const sorted = [ ... durations]. sort (( a , b ) => a - b);
return {
count: durations. length ,
avg: durations. reduce (( a , b ) => a + b, 0 ) / durations. length ,
median: sorted[Math. floor (sorted. length / 2 )],
p95: sorted[Math. floor (sorted. length * 0.95 )],
p99: sorted[Math. floor (sorted. length * 0.99 )],
min: sorted[ 0 ],
max: sorted[sorted. length - 1 ],
};
}
flush () {
const stats : Record < string , any > = {};
this .metrics. forEach (( _ , name ) => {
stats[name] = this . getStats (name);
});
// Send to backend
fetch ( '/api/performance' , {
method: 'POST' ,
body: JSON . stringify (stats),
});
this .metrics. clear ();
}
}
const monitor = new PerformanceMonitor ();
// Global profiler callback
const globalProfilerCallback : ProfilerOnRenderCallback = (
id ,
phase ,
actualDuration
) => {
monitor. recordMetric ( `${ id }-${ phase }` , actualDuration);
};
// Flush metrics periodically
setInterval (() => {
monitor. flush ();
}, 60000 ); // Every minute
// Web Vitals integration
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals' ;
function sendToAnalytics ({ name , delta , id } : any ) {
fetch ( '/api/vitals' , {
method: 'POST' ,
body: JSON . stringify ({ name, value: delta, id }),
});
}
onCLS (sendToAnalytics);
onFID (sendToAnalytics);
onLCP (sendToAnalytics);
onFCP (sendToAnalytics);
onTTFB (sendToAnalytics);
22.2 Bundle Analysis and Code Splitting Strategies
Strategy
Implementation
Bundle Impact
Best For
Route-based Splitting
React.lazy(() => import('./Route'))
Split by page/route
Multi-page applications
Component-based Splitting
Lazy load heavy components
Split by feature/component
Large components, modals, charts
Library Splitting
Dynamic imports for large libs
Vendor bundle optimization
Heavy libraries (PDF, charts, maps)
Webpack Bundle Analyzer
webpack-bundle-analyzer
Visualize bundle composition
Identify optimization opportunities
Tree Shaking
ES6 modules, side-effect-free
Remove unused exports
Reduce bundle size automatically
Prefetching
import(/* webpackPrefetch: true */)
Load during idle time
Improve perceived performance
Example: Advanced code splitting patterns
// Route-based code splitting
import { lazy, Suspense } from 'react' ;
import { Routes, Route } from 'react-router-dom' ;
const Home = lazy (() => import ( './pages/Home' ));
const Dashboard = lazy (() => import ( './pages/Dashboard' ));
const Profile = lazy (() => import ( './pages/Profile' ));
const Settings = lazy (() => import ( './pages/Settings' ));
function App () {
return (
< Suspense fallback = {< LoadingSpinner />}>
< Routes >
< Route path = "/" element = {< Home />} />
< Route path = "/dashboard" element = {< Dashboard />} />
< Route path = "/profile" element = {< Profile />} />
< Route path = "/settings" element = {< Settings />} />
</ Routes >
</ Suspense >
);
}
// Component-based splitting with prefetch
function ProductList () {
const [ showModal , setShowModal ] = useState ( false );
// Lazy load modal component
const ProductModal = lazy (() => import ( './ProductModal' ));
// Prefetch on hover
const handleMouseEnter = () => {
import ( './ProductModal' ); // Prefetch
};
return (
< div >
< button
onClick = {() => setShowModal ( true )}
onMouseEnter = {handleMouseEnter}
>
View Details
</ button >
{showModal && (
< Suspense fallback = {< Spinner />}>
< ProductModal onClose = {() => setShowModal ( false )} />
</ Suspense >
)}
</ div >
);
}
// Library splitting - heavy chart library
const Chart = lazy (() => import ( 'recharts' ). then ( module => ({
default: module .LineChart
})));
function Analytics () {
return (
< Suspense fallback = {< ChartSkeleton />}>
< Chart data = {data} />
</ Suspense >
);
}
// Dynamic import with error handling
function DynamicComponent () {
const [ Component , setComponent ] = useState < React . ComponentType | null >( null );
const [ error , setError ] = useState < Error | null >( null );
useEffect (() => {
import ( './HeavyComponent' )
. then ( module => setComponent (() => module .default))
. catch ( err => setError (err));
}, []);
if (error) return < ErrorMessage error = {error} />;
if ( ! Component) return < Loading />;
return < Component />;
}
// Webpack magic comments for optimization
const AdminPanel = lazy (() =>
import (
/* webpackChunkName: "admin" */
/* webpackPrefetch: true */
'./pages/AdminPanel'
)
);
const ReportGenerator = lazy (() =>
import (
/* webpackChunkName: "reports" */
/* webpackPreload: true */
'./components/ReportGenerator'
)
);
Example: Bundle analysis and optimization
// package.json scripts for bundle analysis
{
"scripts" : {
"analyze" : "webpack-bundle-analyzer build/bundle-stats.json" ,
"build:analyze" : "npm run build -- --stats && npm run analyze"
}
}
// Webpack configuration for bundle optimization
module . exports = {
optimization: {
splitChunks: {
chunks: 'all' ,
cacheGroups: {
// Vendor bundle
vendor: {
test: / [ \\ /] node_modules [ \\ /] / ,
name: 'vendors' ,
priority: 10 ,
},
// Common components
common: {
minChunks: 2 ,
priority: 5 ,
reuseExistingChunk: true ,
},
// React core
react: {
test: / [ \\ /] node_modules [ \\ /] (react | react-dom) [ \\ /] / ,
name: 'react' ,
priority: 20 ,
},
},
},
runtimeChunk: 'single' , // Extract runtime into separate chunk
},
};
// Analyze imports and suggest optimizations
import { lazy } from 'react' ;
// ❌ Bad: Import entire library
import _ from 'lodash' ;
import moment from 'moment' ;
// ✅ Good: Import only what you need
import debounce from 'lodash/debounce' ;
import dayjs from 'dayjs' ; // Lighter alternative
// ❌ Bad: Import all icons
import * as Icons from 'react-icons/fa' ;
// ✅ Good: Import specific icons
import { FaUser, FaHome } from 'react-icons/fa' ;
// Tree shaking configuration in package.json
{
"sideEffects" : [
"*.css" ,
"*.scss"
]
}
// Check bundle size with size-limit
// size-limit.config.js
module . exports = [
{
name: 'Main bundle' ,
path: 'build/static/js/*.js' ,
limit: '200 KB' ,
},
{
name: 'Vendor bundle' ,
path: 'build/static/js/vendors.*.js' ,
limit: '150 KB' ,
},
];
22.3 Memory Leak Detection and Prevention
Leak Source
Detection Method
Prevention
Tools
Event Listeners
Memory snapshots, heap profiling
Remove in cleanup function
Chrome DevTools Memory Profiler
Timers/Intervals
Check running timers count
Clear in useEffect cleanup
React DevTools Profiler
Subscriptions
Monitor active subscriptions
Unsubscribe in cleanup
Custom logging
DOM References
Detached DOM tree analysis
Clear refs, remove listeners
Chrome Memory Profiler
State Updates
Warning: setState on unmounted
Cancel async operations
React strict mode warnings
Closures
Heap snapshot comparison
Avoid large closure scopes
Memory timeline recording
Example: Memory leak prevention patterns
// ✅ Proper event listener cleanup
function WindowResizeComponent () {
const [ width , setWidth ] = useState (window.innerWidth);
useEffect (() => {
const handleResize = () => setWidth (window.innerWidth);
window. addEventListener ( 'resize' , handleResize);
// Cleanup: Remove listener
return () => {
window. removeEventListener ( 'resize' , handleResize);
};
}, []);
return < div >Width: {width}</ div >;
}
// ✅ Timer cleanup
function CountdownTimer () {
const [ count , setCount ] = useState ( 60 );
useEffect (() => {
const intervalId = setInterval (() => {
setCount ( c => c - 1 );
}, 1000 );
// Cleanup: Clear interval
return () => {
clearInterval (intervalId);
};
}, []);
return < div >{count}s remaining</ div >;
}
// ✅ Abort controller for fetch requests
function DataFetcher ({ id } : { id : string }) {
const [ data , setData ] = useState ( null );
useEffect (() => {
const abortController = new AbortController ();
async function fetchData () {
try {
const response = await fetch ( `/api/data/${ id }` , {
signal: abortController.signal,
});
const json = await response. json ();
setData (json);
} catch (error) {
if (error.name === 'AbortError' ) {
console. log ( 'Fetch aborted' );
}
}
}
fetchData ();
// Cleanup: Abort pending request
return () => {
abortController. abort ();
};
}, [id]);
return < div >{ JSON . stringify (data)}</ div >;
}
// ✅ WebSocket cleanup
function WebSocketComponent () {
const [ messages , setMessages ] = useState < string []>([]);
useEffect (() => {
const ws = new WebSocket ( 'ws://localhost:8080' );
ws. onmessage = ( event ) => {
setMessages ( prev => [ ... prev, event.data]);
};
// Cleanup: Close connection
return () => {
ws. close ();
};
}, []);
return < ul >{messages. map (( msg , i ) => < li key = {i}>{msg}</ li >)}</ ul >;
}
// ✅ Observable/RxJS subscription cleanup
import { interval } from 'rxjs' ;
function ObservableComponent () {
const [ tick , setTick ] = useState ( 0 );
useEffect (() => {
const subscription = interval ( 1000 ). subscribe ( val => {
setTick (val);
});
// Cleanup: Unsubscribe
return () => {
subscription. unsubscribe ();
};
}, []);
return < div >Tick: {tick}</ div >;
}
Example: Memory leak detection utilities
// Custom hook to detect memory leaks
function useMemoryLeakDetector ( componentName : string ) {
useEffect (() => {
const mountTime = Date. now ();
console. log ( `[${ componentName }] Mounted` );
return () => {
const unmountTime = Date. now ();
const lifetime = unmountTime - mountTime;
console. log ( `[${ componentName }] Unmounted after ${ lifetime }ms` );
// Check for detached listeners
if (process.env. NODE_ENV === 'development' ) {
setTimeout (() => {
// This will warn if state updates after unmount
console. log ( `[${ componentName }] Cleanup verification` );
}, 100 );
}
};
}, [componentName]);
}
// Safe async state updates
function useSafeState < T >( initialState : T ) {
const [ state , setState ] = useState (initialState);
const isMountedRef = useRef ( true );
useEffect (() => {
return () => {
isMountedRef.current = false ;
};
}, []);
const setSafeState = useCallback (( value : T | (( prev : T ) => T )) => {
if (isMountedRef.current) {
setState (value);
}
}, []);
return [state, setSafeState] as const ;
}
// Usage
function AsyncComponent () {
const [ data , setData ] = useSafeState ( null );
useEffect (() => {
fetchData (). then ( result => {
setData (result); // Safe even if unmounted
});
}, [setData]);
return < div >{ JSON . stringify (data)}</ div >;
}
// Memory profiling helper
class MemoryProfiler {
private snapshots : number [] = [];
takeSnapshot () {
if (performance.memory) {
this .snapshots. push (performance.memory.usedJSHeapSize);
console. log ( `Heap size: ${ ( performance . memory . usedJSHeapSize / 1048576 ). toFixed ( 2 ) } MB` );
}
}
analyze () {
if ( this .snapshots. length < 2 ) return ;
const growth = this .snapshots[ this .snapshots. length - 1 ] - this .snapshots[ 0 ];
const avgGrowth = growth / this .snapshots. length ;
console. log ( `Total growth: ${ ( growth / 1048576 ). toFixed ( 2 ) } MB` );
console. log ( `Average growth per snapshot: ${ ( avgGrowth / 1048576 ). toFixed ( 2 ) } MB` );
}
}
const profiler = new MemoryProfiler ();
// Take snapshots periodically
setInterval (() => profiler. takeSnapshot (), 5000 );
22.4 Re-render Analysis and Optimization
Technique
Implementation
Impact
When to Use
React.memo
Wrap component with memo
Skip re-render if props unchanged
Pure components with expensive renders
useMemo
Memoize expensive calculations
Cache computed values
Heavy computations, derived data
useCallback
Memoize function references
Stable function identity
Callbacks passed to memo'd children
Context Splitting
Separate contexts by update frequency
Reduce context re-render scope
Large context with mixed data
Component Splitting
Break into smaller components
Isolate re-renders
Large components with mixed state
Virtualization
Render only visible items
Massive performance gain for lists
Long lists, tables, grids
Example: Re-render optimization patterns
// ✅ React.memo with custom comparison
interface ItemProps {
item : { id : string ; name : string ; price : number };
onSelect : ( id : string ) => void ;
}
const ListItem = memo (({ item , onSelect } : ItemProps ) => {
console. log ( `Rendering item: ${ item . id }` );
return (
< div onClick = {() => onSelect (item.id)}>
{item.name} - ${item.price}
</ div >
);
}, ( prevProps , nextProps ) => {
// Custom comparison: only re-render if item changed
return prevProps.item.id === nextProps.item.id &&
prevProps.item.name === nextProps.item.name &&
prevProps.item.price === nextProps.item.price;
});
// ✅ useCallback for stable function reference
function ProductList () {
const [ selectedId , setSelectedId ] = useState < string | null >( null );
const [ items , setItems ] = useState (products);
// Memoize callback to prevent ListItem re-renders
const handleSelect = useCallback (( id : string ) => {
setSelectedId (id);
}, []);
return (
< div >
{items. map ( item => (
< ListItem key = {item.id} item = {item} onSelect = {handleSelect} />
))}
</ div >
);
}
// ✅ useMemo for expensive calculations
function DataTable ({ data } : { data : DataPoint [] }) {
// Only recalculate when data changes
const stats = useMemo (() => {
console. log ( 'Calculating stats...' );
return {
total: data. reduce (( sum , d ) => sum + d.value, 0 ),
average: data. reduce (( sum , d ) => sum + d.value, 0 ) / data. length ,
max: Math. max ( ... data. map ( d => d.value)),
min: Math. min ( ... data. map ( d => d.value)),
};
}, [data]);
return (
< div >
< div >Total: {stats.total}</ div >
< div >Average: {stats.average}</ div >
< div >Max: {stats.max}</ div >
< div >Min: {stats.min}</ div >
</ div >
);
}
// ✅ Context splitting to reduce re-renders
// Bad: Single context causes all consumers to re-render
const AppContext = createContext ({
user: null ,
theme: 'light' ,
settings: {},
notifications: [],
});
// Good: Split into separate contexts
const UserContext = createContext ( null );
const ThemeContext = createContext ( 'light' );
const SettingsContext = createContext ({});
const NotificationsContext = createContext ([]);
// Components only re-render when their specific context changes
function UserProfile () {
const user = useContext (UserContext); // Only re-renders on user change
return < div >{user?.name}</ div >;
}
function ThemeSwitcher () {
const theme = useContext (ThemeContext); // Only re-renders on theme change
return < button >{theme}</ button >;
}
Example: Advanced re-render debugging
// Custom hook to track re-renders
function useRenderCount ( componentName : string ) {
const renderCount = useRef ( 0 );
useEffect (() => {
renderCount.current += 1 ;
console. log ( `[${ componentName }] Render count: ${ renderCount . current }` );
});
return renderCount.current;
}
// Hook to track prop changes
function useWhyDidYouUpdate ( name : string , props : Record < string , any >) {
const previousProps = useRef < Record < string , any >>();
useEffect (() => {
if (previousProps.current) {
const allKeys = Object. keys ({ ... previousProps.current, ... props });
const changedProps : Record < string , any > = {};
allKeys. forEach ( key => {
if (previousProps.current ! [key] !== props[key]) {
changedProps[key] = {
from: previousProps.current ! [key],
to: props[key],
};
}
});
if (Object. keys (changedProps). length > 0 ) {
console. log ( '[why-did-you-update]' , name, changedProps);
}
}
previousProps.current = props;
});
}
// Usage in component
function ExpensiveComponent ({ data , config , onUpdate } : Props ) {
const renderCount = useRenderCount ( 'ExpensiveComponent' );
useWhyDidYouUpdate ( 'ExpensiveComponent' , { data, config, onUpdate });
return < div >Rendered {renderCount} times</ div >;
}
// Component splitting for optimization
// ❌ Bad: Entire form re-renders on any input change
function BadForm () {
const [ formData , setFormData ] = useState ({
name: '' ,
email: '' ,
message: '' ,
});
return (
< div >
< input
value = {formData.name}
onChange = { e => setFormData ({ ... formData, name: e.target.value})}
/>
< input
value = {formData.email}
onChange = { e => setFormData ({ ... formData, email: e.target.value})}
/>
< ExpensivePreview data = {formData} />
</ div >
);
}
// ✅ Good: Split into controlled components
const FormInput = memo (({ value , onChange , name } : any ) => {
console. log ( `Rendering input: ${ name }` );
return < input value = {value} onChange = {onChange} />;
});
function GoodForm () {
const [ name , setName ] = useState ( '' );
const [ email , setEmail ] = useState ( '' );
const formData = useMemo (() => ({ name, email }), [name, email]);
return (
< div >
< FormInput name = "name" value = {name} onChange = { e => setName (e.target.value)} />
< FormInput name = "email" value = {email} onChange = { e => setEmail (e.target.value)} />
< ExpensivePreview data = {formData} />
</ div >
);
}
Metric
Target
React Impact
Optimization Strategy
LCP (Largest Contentful Paint)
< 2.5s
Initial render performance
Code splitting, SSR, image optimization
FID (First Input Delay)
< 100ms
JavaScript execution time
Reduce bundle size, defer non-critical JS
CLS (Cumulative Layout Shift)
< 0.1
Dynamic content insertion
Reserve space, avoid layout shifts
FCP (First Contentful Paint)
< 1.8s
Time to first render
Minimize initial bundle, critical CSS
TTFB (Time to First Byte)
< 600ms
Server response time
SSR optimization, CDN, caching
TBT (Total Blocking Time)
< 200ms
Long tasks blocking main thread
Code splitting, Web Workers
Example: Core Web Vitals optimization
// LCP optimization - prioritize hero image
function HeroSection () {
return (
< section >
< img
src = "/hero-large.jpg"
alt = "Hero"
// Prioritize loading
loading = "eager"
fetchpriority = "high"
// Prevent layout shift
width = { 1200 }
height = { 600 }
/>
</ section >
);
}
// FID optimization - defer non-critical code
function App () {
const [ analyticsLoaded , setAnalyticsLoaded ] = useState ( false );
useEffect (() => {
// Load analytics after initial render
requestIdleCallback (() => {
import ( './analytics' ). then ( module => {
module . initAnalytics ();
setAnalyticsLoaded ( true );
});
});
}, []);
return < MainApp />;
}
// CLS optimization - prevent layout shifts
function ImageWithPlaceholder ({ src , alt } : { src : string ; alt : string }) {
const [ loaded , setLoaded ] = useState ( false );
return (
< div style = {{ position: 'relative' , width: 300 , height: 200 }}>
{ /* Placeholder reserves space */ }
{ ! loaded && (
< div
style = {{
position: 'absolute' ,
inset: 0 ,
backgroundColor: '#f0f0f0'
}}
/>
)}
< img
src = {src}
alt = {alt}
width = { 300 }
height = { 200 }
onLoad = {() => setLoaded ( true )}
style = {{ display: loaded ? 'block' : 'none' }}
/>
</ div >
);
}
// Measure Core Web Vitals
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals' ;
function reportWebVitals () {
const sendToAnalytics = ({ name , value , id } : any ) => {
// Send to analytics endpoint
fetch ( '/api/analytics' , {
method: 'POST' ,
body: JSON . stringify ({
metric: name,
value: Math. round (name === 'CLS' ? value * 1000 : value),
id,
timestamp: Date. now (),
}),
keepalive: true ,
});
};
onCLS (sendToAnalytics);
onFID (sendToAnalytics);
onLCP (sendToAnalytics);
onFCP (sendToAnalytics);
onTTFB (sendToAnalytics);
}
// Initialize in app
useEffect (() => {
reportWebVitals ();
}, []);
// Optimize initial load with React.lazy
const HeavyChart = lazy (() =>
import ( /* webpackChunkName: "chart" */ './HeavyChart' )
);
function Dashboard () {
return (
< div >
< Header /> { /* Render immediately */ }
< Suspense fallback = {< ChartSkeleton />}>
< HeavyChart /> { /* Load after initial render */ }
</ Suspense >
</ div >
);
}
// Prevent CLS with skeleton screens
function SkeletonCard () {
return (
< div
style = {{
width: '300px' ,
height: '200px' ,
backgroundColor: '#f0f0f0' ,
borderRadius: '8px' ,
animation: 'pulse 1.5s infinite' ,
}}
/>
);
}
function ProductCard ({ id } : { id : string }) {
const { data , loading } = useFetch < Product >( `/api/products/${ id }` );
if (loading) return < SkeletonCard />; // Same dimensions as actual card
return (
< div style = {{ width: '300px' , height: '200px' }}>
< img src = {data.image} alt = {data.name} />
< h3 >{data.name}</ h3 >
</ div >
);
}
// Optimize TBT with progressive hydration
function ProgressiveApp () {
const [ hydrated , setHydrated ] = useState ({
header: false ,
content: false ,
footer: false ,
});
useEffect (() => {
// Hydrate header first
setHydrated ( prev => ({ ... prev, header: true }));
requestIdleCallback (() => {
// Hydrate content when idle
setHydrated ( prev => ({ ... prev, content: true }));
requestIdleCallback (() => {
// Hydrate footer last
setHydrated ( prev => ({ ... prev, footer: true }));
});
});
}, []);
return (
< div >
{hydrated.header ? < Header /> : < HeaderSSR />}
{hydrated.content ? < Content /> : < ContentSSR />}
{hydrated.footer ? < Footer /> : < FooterSSR />}
</ div >
);
}
Tool
Features
Use Case
Integration
Sentry
Error tracking, performance monitoring, tracing
Production error tracking, performance issues
React SDK, automatic error boundaries
New Relic
APM, browser monitoring, distributed tracing
Full-stack performance monitoring
Browser agent, React instrumentation
Datadog RUM
Real user monitoring, session replay, metrics
User experience analytics
Browser SDK, React integration
LogRocket
Session replay, performance monitoring, logs
Debug production issues with replays
React SDK, Redux integration
Lighthouse CI
Automated performance testing, CI/CD integration
Performance regression prevention
GitHub Actions, GitLab CI
Web Vitals Library
Measure Core Web Vitals in production
Track real user metrics
NPM package, analytics integration
Example: Production monitoring setup
// Sentry integration
import * as Sentry from '@sentry/react' ;
import { BrowserTracing } from '@sentry/tracing' ;
Sentry. init ({
dsn: 'YOUR_SENTRY_DSN' ,
integrations: [
new BrowserTracing (),
new Sentry. Replay ({
maskAllText: true ,
blockAllMedia: true ,
}),
],
// Performance monitoring
tracesSampleRate: 0.1 , // 10% of transactions
// Session replay
replaysSessionSampleRate: 0.1 ,
replaysOnErrorSampleRate: 1.0 ,
// Environment
environment: process.env. NODE_ENV ,
// Release tracking
release: process.env. REACT_APP_VERSION ,
// Custom tags
beforeSend ( event , hint ) {
// Add custom context
event.tags = {
... event.tags,
'user.role' : getCurrentUserRole (),
};
return event;
},
});
// Wrap app with Sentry error boundary
function App () {
return (
< Sentry.ErrorBoundary fallback = {ErrorFallback} showDialog >
< Router />
</ Sentry.ErrorBoundary >
);
}
// Track custom performance metrics
function trackCustomMetric ( name : string , value : number ) {
Sentry.metrics. distribution (name, value, {
unit: 'millisecond' ,
tags: { page: window.location.pathname },
});
}
// Component performance tracking
function ExpensiveComponent () {
useEffect (() => {
const startTime = performance. now ();
return () => {
const duration = performance. now () - startTime;
trackCustomMetric ( 'ExpensiveComponent.renderTime' , duration);
};
});
return < div >...</ div >;
}
// LogRocket setup
import LogRocket from 'logrocket' ;
import setupLogRocketReact from 'logrocket-react' ;
LogRocket. init ( 'YOUR_APP_ID' , {
release: process.env. REACT_APP_VERSION ,
network: {
requestSanitizer : request => {
// Sanitize sensitive data
if (request.headers[ 'Authorization' ]) {
request.headers[ 'Authorization' ] = '[REDACTED]' ;
}
return request;
},
},
});
setupLogRocketReact (LogRocket);
// Connect to Sentry
LogRocket. getSessionURL ( sessionURL => {
Sentry. configureScope ( scope => {
scope. setExtra ( 'sessionURL' , sessionURL);
});
});
// New Relic Browser Agent
// Add to index.html or initialize programmatically
< script src = "https://js-agent.newrelic.com/nr-loader.js" ></ script >
< script >
window.NREUM.init = {
applicationID: 'YOUR_APP_ID' ,
licenseKey: 'YOUR_LICENSE_KEY' ,
// Additional config
};
</ script >
// Datadog RUM
import { datadogRum } from '@datadog/browser-rum' ;
datadogRum. init ({
applicationId: 'YOUR_APP_ID' ,
clientToken: 'YOUR_CLIENT_TOKEN' ,
site: 'datadoghq.com' ,
service: 'react-app' ,
env: process.env. NODE_ENV ,
version: process.env. REACT_APP_VERSION ,
sessionSampleRate: 100 ,
sessionReplaySampleRate: 20 ,
trackUserInteractions: true ,
trackResources: true ,
trackLongTasks: true ,
defaultPrivacyLevel: 'mask-user-input' ,
});
// Start session replay
datadogRum. startSessionReplayRecording ();
// Custom action tracking
function trackUserAction ( name : string , context ?: Record < string , any >) {
datadogRum. addAction (name, context);
LogRocket. track (name, context);
Sentry. addBreadcrumb ({
message: name,
data: context,
});
}
// Web Vitals to all platforms
import { onCLS, onFID, onLCP } from 'web-vitals' ;
function sendToAllPlatforms ( metric : any ) {
// Send to Sentry
Sentry.metrics. distribution (metric.name, metric.value);
// Send to Datadog
datadogRum. addTiming (metric.name, metric.value);
// Send to custom analytics
fetch ( '/api/vitals' , {
method: 'POST' ,
body: JSON . stringify (metric),
keepalive: true ,
});
}
onCLS (sendToAllPlatforms);
onFID (sendToAllPlatforms);
onLCP (sendToAllPlatforms);
// Lighthouse CI configuration
// lighthouserc.js
module . exports = {
ci: {
collect: {
startServerCommand: 'npm run serve' ,
url: [ 'http://localhost:3000' ],
numberOfRuns: 3 ,
},
assert: {
assertions: {
'categories:performance' : [ 'error' , { minScore: 0.9 }],
'categories:accessibility' : [ 'error' , { minScore: 0.9 }],
'first-contentful-paint' : [ 'error' , { maxNumericValue: 2000 }],
'largest-contentful-paint' : [ 'error' , { maxNumericValue: 2500 }],
'cumulative-layout-shift' : [ 'error' , { maxNumericValue: 0.1 }],
},
},
upload: {
target: 'temporary-public-storage' ,
},
},
};
Performance Monitoring Best Practices: Use React Profiler API for component-level insights,
implement code splitting at route and component levels, detect and prevent memory leaks with proper cleanup,
optimize re-renders with memo/useMemo/useCallback, monitor Core Web Vitals in production, integrate multiple
monitoring tools for comprehensive coverage, set up automated performance testing in CI/CD, track custom
business metrics, sanitize sensitive data in monitoring tools, establish performance budgets.