React Hooks revolutionized how we write React components, but most developers only scratch the surface. As a senior developer, I’ve seen teams struggle with hooks patterns that could dramatically improve their code quality and performance. Let’s dive deep into the hooks patterns that separate junior developers from seniors.
Table of Contents
- Why Hooks Patterns Matter
- Custom Hooks: The Secret Weapon
- Performance Hooks: useMemo, useCallback, useRef
- Advanced Patterns
- Common Pitfalls to Avoid
- Real-World Examples
- Best Practices
- Next Steps
Why Hooks Patterns Matter
Hooks are more than just useState
and useEffect
. They’re a paradigm shift that enables:
- Reusable logic across components
- Better performance through optimization
- Cleaner code with less boilerplate
- Better testing with isolated logic
The difference between a junior and senior React developer often comes down to how they use hooks.
Custom Hooks: The Secret Weapon
Basic Custom Hook Pattern
// ❌ BAD: Logic scattered across components
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetchUser()
.then(setUser)
.catch(setError)
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
// ✅ GOOD: Custom hook encapsulates logic
function useUser(userId) {
const [state, setState] = useState({
user: null,
loading: true,
error: null,
});
useEffect(() => {
fetchUser(userId)
.then((user) => setState({ user, loading: false, error: null }))
.catch((error) => setState({ user: null, loading: false, error }));
}, [userId]);
return state;
}
function UserProfile({ userId }) {
const { user, loading, error } = useUser(userId);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
Advanced Custom Hook: useLocalStorage
function 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 reading localStorage key "${key}":`, error);
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 setting localStorage key "${key}":`, error);
}
},
[key, storedValue],
);
return [storedValue, setValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage("theme", "light");
return (
<button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
Current theme: {theme}
</button>
);
}
Performance Hooks: useMemo, useCallback, useRef
useMemo: Expensive Calculations
// ❌ BAD: Recalculates on every render
function ExpensiveComponent({ items, filter }) {
const filteredItems = items.filter((item) =>
item.name.toLowerCase().includes(filter.toLowerCase()),
);
const expensiveResult = filteredItems.reduce((acc, item) => {
// Expensive calculation
return acc + complexCalculation(item);
}, 0);
return <div>{expensiveResult}</div>;
}
// ✅ GOOD: Memoized expensive calculations
function ExpensiveComponent({ items, filter }) {
const filteredItems = useMemo(
() =>
items.filter((item) =>
item.name.toLowerCase().includes(filter.toLowerCase()),
),
[items, filter],
);
const expensiveResult = useMemo(
() =>
filteredItems.reduce((acc, item) => {
return acc + complexCalculation(item);
}, 0),
[filteredItems],
);
return <div>{expensiveResult}</div>;
}
useCallback: Stable Function References
// ❌ BAD: New function on every render
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
console.log("Button clicked");
};
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild onAction={handleClick} />
</div>
);
}
// ✅ GOOD: Stable function reference
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log("Button clicked");
}, []); // Empty dependency array = stable reference
return (
<div>
<button onClick={() => setCount(count + 1)}>Count: {count}</button>
<ExpensiveChild onAction={handleClick} />
</div>
);
}
useRef: Beyond DOM References
// useRef for previous values
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
// useRef for instance variables
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
return <div>Count: {count}</div>;
}
Advanced Patterns
Compound Hooks
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 {
count,
increment,
decrement,
reset,
isZero: count === 0,
};
}
// Usage
function Counter() {
const { count, increment, decrement, reset, isZero } = useCounter(10);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
{isZero && <p>Count is zero!</p>}
</div>
);
}
Async Hooks
function useAsync(asyncFn, immediate = true) {
const [status, setStatus] = useState("idle");
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const execute = useCallback(
async (...args) => {
setStatus("pending");
setData(null);
setError(null);
try {
const response = await asyncFn(...args);
setData(response);
setStatus("success");
} catch (err) {
setError(err);
setStatus("error");
}
},
[asyncFn],
);
useEffect(() => {
if (immediate) {
execute();
}
}, [execute, immediate]);
return { execute, status, data, error };
}
// Usage
function UserList() {
const { data: users, status, error } = useAsync(fetchUsers);
if (status === "pending") return <div>Loading...</div>;
if (status === "error") return <div>Error: {error.message}</div>;
if (status === "success") {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
}
State Machines with Hooks
function useReducer(reducer, initialState) {
const [state, dispatch] = useReducer(reducer, initialState);
const actions = useMemo(
() => ({
start: () => dispatch({ type: "START" }),
success: (data) => dispatch({ type: "SUCCESS", payload: data }),
error: (error) => dispatch({ type: "ERROR", payload: error }),
reset: () => dispatch({ type: "RESET" }),
}),
[],
);
return [state, actions];
}
const fetchReducer = (state, action) => {
switch (action.type) {
case "START":
return { ...state, status: "loading", error: null };
case "SUCCESS":
return { ...state, status: "success", data: action.payload };
case "ERROR":
return { ...state, status: "error", error: action.payload };
case "RESET":
return { status: "idle", data: null, error: null };
default:
return state;
}
};
Common Pitfalls to Avoid
1. Infinite Re-renders
// ❌ BAD: Infinite re-renders
function BadComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Triggers re-render, which triggers useEffect again
});
return <div>{count}</div>;
}
// ✅ GOOD: Proper dependencies
function GoodComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []); // Empty dependency array = run once
return <div>{count}</div>;
}
2. Stale Closures
// ❌ BAD: Stale closure
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial count value
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
// ✅ GOOD: Functional update
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount((c) => c + 1); // Uses current count value
}, 1000);
return () => clearInterval(interval);
}, []);
return <div>{count}</div>;
}
3. Over-optimization
// ❌ BAD: Premature optimization
function OverOptimizedComponent({ items }) {
const processedItems = useMemo(
() => items.map((item) => ({ ...item, processed: true })),
[items],
);
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
return (
<div>
{processedItems.map((item) => (
<button key={item.id} onClick={handleClick}>
{item.name}
</button>
))}
</div>
);
}
// ✅ GOOD: Optimize when needed
function OptimizedComponent({ items }) {
// Only memoize if items array is large or processing is expensive
const processedItems =
items.length > 1000
? useMemo(
() => items.map((item) => ({ ...item, processed: true })),
[items],
)
: items.map((item) => ({ ...item, processed: true }));
return (
<div>
{processedItems.map((item) => (
<button key={item.id} onClick={() => console.log("clicked")}>
{item.name}
</button>
))}
</div>
);
}
Real-World Examples
E-commerce Product Filter
function useProductFilter(products) {
const [filters, setFilters] = useState({
category: "all",
priceRange: [0, 1000],
sortBy: "name",
});
const filteredProducts = useMemo(() => {
return products
.filter(
(product) =>
filters.category === "all" || product.category === filters.category,
)
.filter(
(product) =>
product.price >= filters.priceRange[0] &&
product.price <= filters.priceRange[1],
)
.sort((a, b) => {
if (filters.sortBy === "name") return a.name.localeCompare(b.name);
if (filters.sortBy === "price") return a.price - b.price;
return 0;
});
}, [products, filters]);
const updateFilter = useCallback((key, value) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
return {
filters,
filteredProducts,
updateFilter,
};
}
Best Practices
1. Hook Naming Convention
// Always start with "use"
function useUserData() {
/* ... */
}
function useLocalStorage() {
/* ... */
}
function useAsyncOperation() {
/* ... */
}
2. Dependency Arrays
// Be explicit about dependencies
useEffect(() => {
// effect logic
}, [dependency1, dependency2]); // List all dependencies
// Use ESLint rule: react-hooks/exhaustive-deps
3. Custom Hook Composition
// Compose hooks for complex logic
function useUserProfile(userId) {
const { data: user, loading, error } = useUser(userId);
const { data: posts } = useUserPosts(userId);
const { data: followers } = useUserFollowers(userId);
return {
user,
posts,
followers,
loading: loading || !user || !posts || !followers,
error,
};
}
Next Steps
1. Advanced Hook Libraries
- React Query for server state management
- SWR for data fetching
- React Hook Form for form management
2. Performance Monitoring
- Use React DevTools Profiler
- Monitor re-renders with Why Did You Render
- Measure hook performance with custom timing
3. Testing Custom Hooks
import { renderHook, act } from "@testing-library/react";
test("useCounter increments correctly", () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
4. Related Learning Resources
- React Performance Optimization - Apply hooks for better performance
- Next.js App Router - Modern React patterns
- JavaScript Performance - Optimize data operations
Conclusion
Mastering React hooks patterns is what separates junior developers from seniors. The key is understanding not just how to use hooks, but when and why to use them.
Key Takeaways for Junior Developers:
- Custom hooks are your friend - Extract reusable logic
- Performance matters - Use
useMemo
anduseCallback
wisely - Avoid common pitfalls - Stale closures, infinite re-renders
- Compose hooks - Build complex logic from simple hooks
- Test your hooks - Ensure reliability and maintainability
Remember: Hooks are not just a feature, they’re a mindset. Think in terms of reusable, composable logic rather than component-specific code.
Related Articles:
- Optimizing React Performance - Apply these patterns for better performance
- Next.js App Router Patterns - Modern React development
- JavaScript Performance - Optimize data operations
Start building better React applications today! 🚀