React Custom Hooks - Complete Deep Dive Guide
Mahesh Waghmare Custom hooks are one of React’s most powerful features for code reuse and logic sharing. This comprehensive guide covers everything from basic custom hooks to advanced patterns and best practices.
Introduction to Custom Hooks
Custom hooks let you extract component logic into reusable functions. They’re regular JavaScript functions that can use other hooks.
Benefits:
- Code reusability
- Logic separation
- Easier testing
- Better organization
- Shared stateful logic
Naming Convention:
- Must start with “use”
- Examples:
useCounter,useFetch,useLocalStorage
Rules of Hooks
Essential Rules:
- Only call hooks at the top level
- Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React function components or custom hooks
- Custom hooks must start with "use"
Example - Correct:
function MyComponent() {
const [count, setCount] = useState(0);
const data = useFetch('/api/data');
return <div>{count}</div>;
}
Example - Incorrect:
function MyComponent() {
if (condition) {
const [count, setCount] = useState(0); // Wrong!
}
return <div>{count}</div>;
}
Creating Custom Hooks
Basic Custom Hook
useCounter Hook:
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Usage
function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Hook with Side Effects
useFetch Hook:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(url)
.then(res => res.json())
.then(data => {
setData(data);
setError(null);
})
.catch(err => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
}
// Usage
function DataComponent() {
const { data, loading, error } = useFetch('/api/data');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}
Common Hook Patterns
useLocalStorage Hook
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];
}
useDebounce Hook
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
useToggle Hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(v => !v);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
Advanced Hook Patterns
useReducer Hook Pattern
function useAsync(asyncFunction) {
const [state, dispatch] = useReducer(asyncReducer, {
data: null,
loading: false,
error: null,
});
const execute = useCallback(async (...args) => {
dispatch({ type: 'START' });
try {
const data = await asyncFunction(...args);
dispatch({ type: 'SUCCESS', payload: data });
} catch (error) {
dispatch({ type: 'ERROR', payload: error });
}
}, [asyncFunction]);
return { ...state, execute };
}
Composing Hooks
function useUserData(userId) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
const [preferences, setPreferences] = useLocalStorage(`user-${userId}-prefs`, {});
return {
user,
preferences,
loading,
error,
updatePreferences: setPreferences,
};
}
Testing Custom Hooks
Using React Testing Library
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
test('increments count', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Conclusion
Custom hooks enable:
- Reusable component logic
- Better code organization
- Easier testing
- Shared stateful logic
Key principles:
- Start with “use”
- Follow hooks rules
- Return values consistently
- Handle edge cases
- Document hook behavior
Mastering custom hooks significantly improves React development efficiency and code quality.
Written by Mahesh Waghmare
I bridge the gap between WordPress architecture and modern React frontends. Currently building tools for the AI era.
Follow on Twitter →