Introduction
React Hooks have revolutionized the way we write React components since their introduction in version 16.8. However, many developers still use hooks only at a surface level, without realizing the powerful patterns they enable.
This article explores advanced React Hooks patterns that can significantly improve the quality, performance, and scalability of your React applications.
Powerful Custom Hooks
useFetchwith Cache Management
import { useState, useEffect, useRef } from "react";
const useFetch = (url, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const cache = useRef({});
useEffect(() => {
if (!url) return;
const fetchData = async () => {
setLoading(true);
setError(null);
// Cache mechanism
if (cache.current[url]) {
setData(cache.current[url]);
setLoading(false);
return;
}
try {
const response = await fetch(url, options);
if (!response.ok) throw new Error("Network response was not ok");
const result = await response.json();
cache.current[url] = result;
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
};
2. useLocalStorage with Serialization
import { useState } from "react";
const 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 = (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);
}
};
return [storedValue, setValue];
};
useReducer for Complex State Management
Advanced Todo App with useReducer
const todoReducer = (state, action) => {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [
...state.todos,
{
id: Date.now(),
text: action.payload,
completed: false,
createdAt: new Date().toISOString(),
},
],
};
case "TOGGLE_TODO":
return {
...state,
todos: state.todos.map((todo) =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
),
};
case "DELETE_TODO":
return {
...state,
todos: state.todos.filter((todo) => todo.id !== action.payload),
};
case "SET_FILTER":
return { ...state, filter: action.payload };
case "BULK_UPDATE":
return {
...state,
todos: state.todos.map((todo) => ({
...todo,
...action.payload.updates,
})),
};
default:
return state;
}
};
const useTodo = () => {
const [state, dispatch] = useReducer(todoReducer, {
todos: [],
filter: "all",
});
const filteredTodos = state.todos.filter((todo) => {
switch (state.filter) {
case "completed":
return todo.completed;
case "active":
return !todo.completed;
default:
return true;
}
});
return {
todos: filteredTodos,
addTodo: (text) => dispatch({ type: "ADD_TODO", payload: text }),
toggleTodo: (id) => dispatch({ type: "TOGGLE_TODO", payload: id }),
deleteTodo: (id) => dispatch({ type: "DELETE_TODO", payload: id }),
setFilter: (filter) => dispatch({ type: "SET_FILTER", payload: filter }),
};
};
Optimization Techniques That Are Rarely Known
1. useMemo with Deep Comparison
import { useMemo, useRef } from "react";
const useDeepCompareMemo = (factory, dependencies) => {
const ref = useRef();
const serializedDeps = JSON.stringify(dependencies);
return useMemo(() => {
if (ref.current !== serializedDeps) {
ref.current = serializedDeps;
return factory();
}
}, [serializedDeps, factory]);
};
2. useCallback with Dependency Injection
import { useCallback, useEffect, useRef } from "react";
const useStableCallback = (callback, dependencies = []) => {
const callbackRef = useRef(callback);
useEffect(() => {
callbackRef.current = callback;
});
return useCallback((...args) => {
return callbackRef.current(...args);
}, dependencies);
};
Advanced Pattern: Compound Components with Context
import { createContext, useContext, useState } from "react";
const AccordionContext = createContext();
const Accordion = ({ children, multiple = false }) => {
const [openItems, setOpenItems] = useState([]);
const toggleItem = (id) => {
setOpenItems((prev) =>
multiple
? prev.includes(id)
? prev.filter((i) => i !== id)
: [...prev, id]
: prev.includes(id)
? []
: [id]
);
};
return (
<AccordionContext.Provider value={{ openItems, toggleItem }}>
{children}
</AccordionContext.Provider>
);
};
Pattern: State Machine with useReducer
const createMachine = (config) => (state, action) => {
const current = config.states[state.state];
if (current?.transitions?.[action.type]) {
const transition = current.transitions[action.type];
return {
state: transition.target,
context:
transition.action?.(state.context, action) ?? state.context,
};
}
return state;
};
Best Practices and Pitfalls
1. Hooks Dependency Array
// ❌ Wrong
useEffect(() => {}, [props.user]);
// ✅ Correct
useEffect(() => {}, [props.user.id]);
2. Custom Hooks Composition
const useUserDashboard = (userId) => {
const user = useUser(userId);
const posts = useUserPosts(userId);
const notifications = useUserNotifications(userId);
const isLoading =
user.loading || posts.loading || notifications.loading;
return {
user: user.data,
posts: posts.data,
notifications: notifications.data,
isLoading,
};
};
3. Error Handling with Hooks
import { useCallback, useEffect, useState } from "react";
const useErrorHandler = () => {
const [error, setError] = useState(null);
const handleError = useCallback((err) => {
console.error("Application error:", err);
setError(err);
}, []);
useEffect(() => {
if (error) throw error;
}, [error]);
return handleError;
};
Conclusion
Advanced React Hooks patterns unlock powerful ways to build maintainable, performant, and scalable applications.
Key Takeaways
Custom hooks improve logic reuse and separation of concerns
useReducersimplifies complex state managementAdvanced memoization boosts performance
Compound components enable flexible APIs
State machines create predictable state transitions
By mastering these patterns, you will become a more confident and effective React developer.