Advanced React Hooks: Beyond useState and useEffect
Sarah Chen

Explore powerful React hooks like useReducer, useContext, and custom hooks to build more maintainable and efficient React applications.
React Hooks have revolutionized how we write React components, moving away from class components to functional components with state and lifecycle methods. While most developers are familiar with useState
and useEffect
, React offers several other powerful hooks that can significantly improve your application’s architecture and performance.
useReducer: Managing Complex State
When your component’s state logic becomes complex, useReducer
provides a more predictable way to manage state transitions.
"text-[#66d9ef]">import { useReducer } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">class="text-[#75715e] italic">// Define the initial state
"text-[#66d9ef]">const initialState = {
count: 0,
loading: "text-[#ae81ff]">false,
error: "text-[#ae81ff]">null
};
"text-[#66d9ef]">class="text-[#75715e] italic">// Define the reducer "text-[#66d9ef]">function
"text-[#66d9ef]">function counterReducer(state, action) {
switch (action."text-[#66d9ef]">type) {
case 'increment':
"text-[#66d9ef]">return { ...state, count: state.count + 1 };
case 'decrement':
"text-[#66d9ef]">return { ...state, count: state.count - 1 };
case 'reset':
"text-[#66d9ef]">return { ...state, count: 0 };
case 'set_loading':
"text-[#66d9ef]">return { ...state, loading: action.payload };
case 'set_error':
"text-[#66d9ef]">return { ...state, error: action.payload };
"text-[#66d9ef]">default:
"text-[#66d9ef]">throw "text-[#66d9ef]">new Error(`Unknown action "text-[#66d9ef]">type: ${action."text-[#66d9ef]">type}`);
}
}
"text-[#66d9ef]">function Counter() {
"text-[#66d9ef]">const [state, dispatch] = useReducer(counterReducer, initialState);
"text-[#66d9ef]">return (
Count: {state.count}
{state.loading && Loading...
}
{state.error && Error: {state.error}
}
);
}
useContext: Avoiding Prop Drilling
useContext
allows you to consume context values without wrapping your component in a Consumer component, making your code cleaner and more readable.
"text-[#66d9ef]">import { createContext, useContext, useState } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">class="text-[#75715e] italic">// Create the context
"text-[#66d9ef]">const ThemeContext = createContext();
"text-[#66d9ef]">class="text-[#75715e] italic">// Theme provider component
"text-[#66d9ef]">function ThemeProvider({ children }) {
"text-[#66d9ef]">const [theme, setTheme] = useState('light');
"text-[#66d9ef]">const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
"text-[#66d9ef]">return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
ThemeContext.Provider>
);
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Custom hook to use theme context
"text-[#66d9ef]">function useTheme() {
"text-[#66d9ef]">const context = useContext(ThemeContext);
"text-[#66d9ef]">if (context === "text-[#ae81ff]">undefined) {
"text-[#66d9ef]">throw "text-[#66d9ef]">new Error('useTheme must be used within a ThemeProvider');
}
"text-[#66d9ef]">return context;
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Component using the context
"text-[#66d9ef]">function Header() {
"text-[#66d9ef]">const { theme, toggleTheme } = useTheme();
"text-[#66d9ef]">return (
`header ${theme}`}>
My App
);
}
useMemo: Optimizing Expensive Calculations
useMemo
helps prevent expensive calculations from running on every render by memoizing the result.
"text-[#66d9ef]">import { useMemo, useState } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">function ExpensiveComponent({ items, filterText }) {
"text-[#66d9ef]">class="text-[#75715e] italic">// Expensive filtering operation
"text-[#66d9ef]">const filteredItems = useMemo(() => {
console.log('Filtering items...');
"text-[#66d9ef]">return items.filter(item =>
item.name.toLowerCase().includes(filterText.toLowerCase())
).sort((a, b) => a.name.localeCompare(b.name));
}, [items, filterText]); "text-[#66d9ef]">class="text-[#75715e] italic">// Only recalculate when dependencies change
"text-[#66d9ef]">const expensiveValue = useMemo(() => {
"text-[#66d9ef]">class="text-[#75715e] italic">// Simulate expensive calculation
"text-[#66d9ef]">let result = 0;
"text-[#66d9ef]">for ("text-[#66d9ef]">let i = 0; i < 1000000; i++) {
result += Math.random();
}
"text-[#66d9ef]">return result;
}, []); "text-[#66d9ef]">class="text-[#75715e] italic">// Empty dependency array means this runs only once
"text-[#66d9ef]">return (
Expensive value: {expensiveValue.toFixed(2)}
{filteredItems.map(item => (
- {item.name}
))}
);
}
useCallback: Preventing Unnecessary Re-renders
useCallback
returns a memoized version of the callback function that only changes if one of the dependencies has changed.
"text-[#66d9ef]">import { useCallback, useState, memo } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">class="text-[#75715e] italic">// Child component wrapped with memo "text-[#66d9ef]">for optimization
"text-[#66d9ef]">const TodoItem = memo(({ todo, onToggle, onDelete }) => {
console.log('TodoItem rendered:', todo.text);
"text-[#66d9ef]">return (
"text-[#66d9ef]">type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
'completed' : ''}>
{todo.text}
);
});
"text-[#66d9ef]">function TodoList() {
"text-[#66d9ef]">const [todos, setTodos] = useState([]);
"text-[#66d9ef]">const [filter, setFilter] = useState('all');
"text-[#66d9ef]">class="text-[#75715e] italic">// Memoized callbacks prevent child re-renders
"text-[#66d9ef]">const handleToggle = useCallback((id) => {
setTodos(prevTodos =>
prevTodos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
)
);
}, []);
"text-[#66d9ef]">const handleDelete = useCallback((id) => {
setTodos(prevTodos => prevTodos.filter(todo => todo.id !== id));
}, []);
"text-[#66d9ef]">const addTodo = useCallback((text) => {
"text-[#66d9ef]">const newTodo = {
id: Date.now(),
text,
completed: "text-[#ae81ff]">false
};
setTodos(prevTodos => [...prevTodos, newTodo]);
}, []);
"text-[#66d9ef]">return (
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
/>
))}
);
}
Custom Hooks: Reusable Stateful Logic
Custom hooks allow you to extract component logic into reusable functions.
"text-[#66d9ef]">import { useState, useEffect } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">class="text-[#75715e] italic">// Custom hook "text-[#66d9ef]">for fetching data
"text-[#66d9ef]">function useFetch(url) {
"text-[#66d9ef]">const [data, setData] = useState("text-[#ae81ff]">null);
"text-[#66d9ef]">const [loading, setLoading] = useState("text-[#ae81ff]">true);
"text-[#66d9ef]">const [error, setError] = useState("text-[#ae81ff]">null);
useEffect(() => {
"text-[#66d9ef]">const fetchData = "text-[#66d9ef]">async () => {
"text-[#66d9ef]">try {
setLoading("text-[#ae81ff]">true);
"text-[#66d9ef]">const response = "text-[#66d9ef]">await fetch(url);
"text-[#66d9ef]">if (!response.ok) {
"text-[#66d9ef]">throw "text-[#66d9ef]">new Error(`HTTP error! status: ${response.status}`);
}
"text-[#66d9ef]">const result = "text-[#66d9ef]">await response.json();
setData(result);
} "text-[#66d9ef]">catch (err) {
setError(err.message);
} finally {
setLoading("text-[#ae81ff]">false);
}
};
fetchData();
}, [url]);
"text-[#66d9ef]">return { data, loading, error };
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Custom hook "text-[#66d9ef]">for local storage
"text-[#66d9ef]">function useLocalStorage(key, initialValue) {
"text-[#66d9ef]">const [storedValue, setStoredValue] = useState(() => {
"text-[#66d9ef]">try {
"text-[#66d9ef]">const item = window.localStorage.getItem(key);
"text-[#66d9ef]">return item ? JSON.parse(item) : initialValue;
} "text-[#66d9ef]">catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
"text-[#66d9ef]">return initialValue;
}
});
"text-[#66d9ef]">const setValue = (value) => {
"text-[#66d9ef]">try {
"text-[#66d9ef]">const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} "text-[#66d9ef]">catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
};
"text-[#66d9ef]">return [storedValue, setValue];
}
"text-[#66d9ef]">class="text-[#75715e] italic">// Using custom hooks in a component
"text-[#66d9ef]">function UserProfile({ userId }) {
"text-[#66d9ef]">const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
"text-[#66d9ef]">const [preferences, setPreferences] = useLocalStorage('userPreferences', {
theme: 'light',
notifications: "text-[#ae81ff]">true
});
"text-[#66d9ef]">if (loading) "text-[#66d9ef]">return Loading...;
"text-[#66d9ef]">if (error) "text-[#66d9ef]">return Error: {error};
"text-[#66d9ef]">return (
{user.name}
Email: {user.email}
Preferences
);
}
useRef: Accessing DOM Elements and Persistent Values
useRef
provides a way to access DOM elements directly and store mutable values that persist across renders.
"text-[#66d9ef]">import { useRef, useEffect, useState } "text-[#66d9ef]">from 'react';
"text-[#66d9ef]">function FocusableInput() {
"text-[#66d9ef]">const inputRef = useRef("text-[#ae81ff]">null);
"text-[#66d9ef]">const [count, setCount] = useState(0);
"text-[#66d9ef]">const renderCountRef = useRef(0);
"text-[#66d9ef]">class="text-[#75715e] italic">// Focus input on mount
useEffect(() => {
inputRef.current?.focus();
}, []);
"text-[#66d9ef]">class="text-[#75715e] italic">// Track render count without causing re-renders
useEffect(() => {
renderCountRef.current += 1;
});
"text-[#66d9ef]">const handleFocus = () => {
inputRef.current?.focus();
};
"text-[#66d9ef]">return (
Component has rendered {renderCountRef.current} times
Count: {count}
"text-[#66d9ef]">type="text"
placeholder="This input will be focused on mount"
/>
);
}
Best Practices for React Hooks
1. Follow the Rules of Hooks
- Only call hooks at the top level of your React function
- Only call hooks from React function components or custom hooks
- Use the ESLint plugin
eslint-plugin-react-hooks
to enforce these rules
2. Optimize Dependencies
Always include all values from component scope that are used inside the effect in the dependencies array:
"text-[#66d9ef]">class="text-[#75715e] italic">// ❌ Missing dependency
useEffect(() => {
fetchUser(userId);
}, []); "text-[#66d9ef]">class="text-[#75715e] italic">// userId is missing "text-[#66d9ef]">from dependencies
"text-[#66d9ef]">class="text-[#75715e] italic">// ✅ Correct dependencies
useEffect(() => {
fetchUser(userId);
}, [userId]);
3. Custom Hook Naming
Always start custom hook names with use
to follow React conventions:
"text-[#66d9ef]">class="text-[#75715e] italic">// ✅ Good
"text-[#66d9ef]">function useUserData(userId) { "text-[#66d9ef]">class="text-[#75715e] italic">/* ... */ }
"text-[#66d9ef]">function useLocalStorage(key) { "text-[#66d9ef]">class="text-[#75715e] italic">/* ... */ }
"text-[#66d9ef]">class="text-[#75715e] italic">// ❌ Bad
"text-[#66d9ef]">function getUserData(userId) { "text-[#66d9ef]">class="text-[#75715e] italic">/* ... */ }
"text-[#66d9ef]">function localStorage(key) { "text-[#66d9ef]">class="text-[#75715e] italic">/* ... */ }
Conclusion
Advanced React hooks like useReducer
, useContext
, useMemo
, useCallback
, and custom hooks provide powerful tools for building efficient and maintainable React applications. By understanding when and how to use these hooks, you can:
- Manage complex state more predictably
- Avoid prop drilling with context
- Optimize performance with memoization
- Create reusable stateful logic with custom hooks
- Access DOM elements and persist values with useRef
Remember that with great power comes great responsibility – use these hooks judiciously and always consider the trade-offs between complexity and performance. The key is to start simple and add optimization only when needed.