React Hooks Fundamentals
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. 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.
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 >;
};
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.
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.
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.