
React Hooks: Advanced Patterns and Best Practices
Since the introduction of React Hooks in React 16.8, the way developers manage state and lifecycle in functional components has completely transformed.
Instead of writing complex class components, we can now handle state, side effects, context, and even performance optimizations directly inside functional components.
But to truly harness the power of Hooks, it's not enough to just know useState
and useEffect
— you need to explore advanced patterns and best practices that make applications scalable, maintainable, and performant.
Why Advanced Hook Patterns Matter
Many developers start with basic hooks for simple tasks like toggling UI state or fetching data. However, as applications grow, state management becomes more complex, performance bottlenecks arise, and reusability becomes a major challenge. Advanced hook patterns provide solutions to these real-world problems by improving code organization, reducing duplication, and boosting app performance.
1. Creating Custom Hooks
Custom Hooks are one of the most powerful patterns in React. They allow you to extract reusable logic from components and share it across different parts of your app.
A custom hook is simply a function that uses other React hooks and follows the naming convention useSomething
.
// Example: useFetch custom hook
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// Usage in a component
function UserList() {
const { data: users, loading } = useFetch("/api/users");
if (loading) return <p>Loading...</p>;
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This pattern eliminates repetitive code and improves testability.
For example, the useFetch
hook can now be reused for fetching users, products, posts, or any other resource.
2. useReducer for Complex State
While useState
is perfect for simple state variables, managing complex state transitions often requires more control.
This is where useReducer
shines — it centralizes state logic into a reducer function, similar to Redux but at a component level.
import { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
case "reset":
return initialState;
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}
This pattern makes your state management more predictable and is ideal for forms, wizards, or any component with multiple related state transitions.
3. Performance Optimization with useMemo & useCallback
React re-renders components whenever props or state change. While this is usually fine, it can lead to performance issues in large applications.
Two hooks — useMemo
and useCallback
— help optimize expensive computations and prevent unnecessary re-renders.
- useMemo: Memoizes expensive calculations so they're only re-computed when dependencies change.
- useCallback: Memoizes callback functions so child components don’t re-render unnecessarily.
import { useMemo, useCallback, useState } from "react";
function ExpensiveComponent({ items }) {
const [count, setCount] = useState(0);
const sortedItems = useMemo(() => {
console.log("Sorting items...");
return [...items].sort();
}, [items]);
const increment = useCallback(() => setCount((c) => c + 1), []);
return (
<div>
<button onClick={increment}>Increment {count}</button>
<ul>
{sortedItems.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
}
Without these optimizations, React might re-sort items or recreate callback functions on every render, causing performance issues.
4. Rules and Pitfalls of Hooks
- Always call hooks at the top level (never inside loops, conditions, or nested functions).
- Only call hooks from React functions (functional components or custom hooks).
- Don’t over-optimize — useMemo/useCallback everywhere can hurt readability without real performance gains.
5. Combining Patterns in Real-world Applications
Advanced applications often use a mix of these patterns. For example:
- A Custom Hook that internally uses
useReducer
for state transitions. - Using
useMemo
to cache derived state like filtered lists. - Using
useCallback
to pass stable event handlers into deeply nested components.
Conclusion
Mastering React Hooks goes beyond knowing the basics — it’s about applying advanced patterns like custom hooks, reducers, and performance optimizations to build scalable applications. By following best practices and being mindful of pitfalls, you can write cleaner, faster, and more maintainable React code.
✨ Tags
About Maria Garcia
Maria is a React engineer specializing in advanced state management.