React Hooks - useState, useEffect, useContext
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 (lifecycle methods)
| 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 API
| 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 for Performance Optimization
| 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 Access and Mutable Values
| 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.