Custom React Hooks Patterns
Reusable logic patterns with custom React hooks for cleaner, more maintainable components
Introduction
Custom hooks are one of React's most powerful features. They let you extract component logic into reusable functions, promoting code reuse and cleaner components.
What Are Custom Hooks?
Custom hooks are JavaScript functions that start with use and call other hooks.
// Custom hook for tracking window size
function useWindowSize() {
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);
}, []);
return size;
}
Common Patterns
useGetLatest
Keep the latest value of a variable without causing re-renders.
function useGetLatest(value) {
const ref = useRef();
ref.current = value;
return useCallback(() => ref.current, []);
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const getLatestCount = useGetLatest(count);
const logCount = () => {
console.log(getLatestCount()); // Always latest
};
return <button onClick={logCount}>Log</button>;
}
useAsyncDebounce
Debounce async operations to prevent excessive calls.
function useAsyncDebounce(defaultFn, defaultWait = 0) {
const debounceRef = useRef({});
const getDefaultFn = useGetLatest(defaultFn);
const getDefaultWait = useGetLatest(defaultWait);
return useCallback(
async (...args) => {
if (!debounceRef.current.promise) {
debounceRef.current.promise = new Promise((resolve, reject) => {
debounceRef.current.resolve = resolve;
debounceRef.current.reject = reject;
});
}
if (debounceRef.current.timeout) {
clearTimeout(debounceRef.current.timeout);
}
debounceRef.current.timeout = setTimeout(async () => {
delete debounceRef.current.timeout;
try {
debounceRef.current.resolve(await getDefaultFn()(...args));
} catch (err) {
debounceRef.current.reject(err);
} finally {
delete debounceRef.current.promise;
}
}, getDefaultWait());
return debounceRef.current.promise;
},
[getDefaultFn, getDefaultWait],
);
}
// Usage
function SearchBox() {
const [results, setResults] = useState([]);
const search = useAsyncDebounce(async (query) => {
const response = await fetch(`/api/search?q=${query}`);
return response.json();
}, 300);
const handleChange = async (e) => {
const results = await search(e.target.value);
setResults(results);
};
return <input onChange={handleChange} />;
}
useLocalStorage
Persist state in localStorage.
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggle = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return <button onClick={toggle}>{theme}</button>;
}
useFetch
Simplify data fetching with built-in loading and error states.
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
});
if (!response.ok) throw new Error('Network error');
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{user.name}</div>;
}
Best Practices
Follow Hook Rules
- Only call hooks at the top level
- Only call hooks from React functions
- Name custom hooks with
useprefix
Keep Hooks Focused
Each hook should do one thing well.
// ✅ Focused
function useUser() {
/* ... */
}
function usePosts() {
/* ... */
}
// ❌ Too broad
function useEverything() {
/* user, posts, comments, settings... */
}
Document Your Hooks
/**
* useLocalStorage - Persist state in localStorage
* @param {string} key - localStorage key
* @param {any} initialValue - Initial value if key doesn't exist
* @returns {[any, function]} Current value and setter function
*/
function useLocalStorage(key, initialValue) {
// ...
}
Conclusion
Custom hooks are essential for building reusable, maintainable React applications. Master these patterns to write cleaner components.
#react #hooks #javascript