Building Better React Apps with Custom Hooks: From Basics to Advanced Patterns
In earlier articles, we have covered the basics of React Custom Hooks and how to use them in our projects. If you haven't read them yet, you can find them Complete Guide to Custom Hooks with Examples.
In this article, we'll cover some more examples of custom hooks in ReactJS, best practices, factory pattern as well as testing a them and how to use them in our projects.
1. Understanding the Rules of Hooks
Before diving deeper into custom hooks, it's crucial to understand the fundamental rules that govern how they work in React:
Rule 1: Only Call Hooks at the Top Level
// ❌ Wrong - Hook inside condition
function Component() {
if (someCondition) {
const [state, setState] = useState(0);
}
}
// ✅ Correct - Hook at top level
function Component() {
const [state, setState] = useState(0);
if (someCondition) {
// Use state here
}
}
Rule 2: Only Call Hooks from React Functions
// ❌ Wrong - Hook in regular function
function regularFunction() {
const [state, setState] = useState(0);
}
// ✅ Correct - Hook in React component or custom hook
function useCustomHook() {
const [state, setState] = useState(0);
return state;
}
2. Building Progressive Custom Hooks
Let's build custom hooks step by step, starting with simple examples and progressing to more complex ones.
2.1 useToggle: Your First Custom Hook
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
return [value, toggle];
}
// Usage Example
function ExpandableSection() {
const [isExpanded, toggleExpanded] = useToggle(false);
return (
<div>
<button onClick={toggleExpanded}>
{isExpanded ? 'Show Less' : 'Show More'}
</button>
{isExpanded && (
<div>Extra content here...</div>
)}
</div>
);
}
2.2 useDebounce: Managing Input Updates
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
// Usage Example
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
// Only search when debounced value changes
if (debouncedSearchTerm) {
performSearch(debouncedSearchTerm);
}
}, [debouncedSearchTerm]);
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
3. Advanced Custom Hook Example: useLocalStorage
The useLocalStorage
hook is a custom hook that manages state stored in localStorage
, allowing data persistence across page reloads.
import { useState } from 'react';
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);
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);
}
};
return [storedValue, setValue];
}
// Usage of useLocalStorage in a component
function ThemeSwitcher() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</div>
);
}
4. Advanced Hook Patterns
4.1 Composition of Hooks
Custom hooks can be composed together to create more powerful functionality:
function useAuthenticatedApi() {
const { token } = useAuth(); // Custom auth hook
const axiosInstance = useMemo(() => {
return axios.create({
headers: { Authorization: `Bearer ${token}` }
});
}, [token]);
const makeRequest = useCallback(async (config) => {
try {
const response = await axiosInstance(config);
return { data: response.data, error: null };
} catch (error) {
return { data: null, error };
}
}, [axiosInstance]);
return { makeRequest };
}
4.2 Hook Factory Pattern
Sometimes you need to create variations of a hook based on configuration:
function createApiHook(baseURL) {
return function useApi() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const fetchData = useCallback(async (endpoint) => {
setLoading(true);
try {
const response = await fetch(`${baseURL}${endpoint}`);
const json = await response.json();
setData(json);
} finally {
setLoading(false);
}
}, []);
return { data, loading, fetchData };
};
}
// Create specific API hooks
const useUsersApi = createApiHook('https://api.example.com/users');
const useProductsApi = createApiHook('https://api.example.com/products');
5. Testing Custom Hooks
Testing custom hooks requires special consideration. Here's how to effectively test them:
import { renderHook, act } from '@testing-library/react-hooks';
// Test for useToggle hook
describe('useToggle', () => {
it('should toggle value', () => {
const { result } = renderHook(() => useToggle(false));
expect(result.current[0]).toBe(false);
act(() => {
result.current[1]();
});
expect(result.current[0]).toBe(true);
});
});
6. Performance Optimization Tips
When working with custom hooks, consider these optimization strategies:
- Memoization of Values:
function useCalculation(value) {
const expensiveValue = useMemo(() => {
return performExpensiveCalculation(value);
}, [value]);
return expensiveValue;
}
- Callback Memoization:
function useEventHandler() {
const [data, setData] = useState(null);
const handleEvent = useCallback((event) => {
setData(event.target.value);
}, []); // Empty deps array since it doesn't depend on any values
return { data, handleEvent };
}
- Avoiding Unnecessary Re-renders:
function useDataFetching() {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
// Better than having separate useState for each property
const updateState = useCallback((updates) => {
setState(prev => ({ ...prev, ...updates }));
}, []);
return [state, updateState];
}
7. Real-World Applications
Let's look at some practical scenarios where custom hooks shine:
- Form Management
- Authentication
- API Integration
- WebSocket Connections
- Browser APIs (localStorage, mediaQueries, etc.)
- Animation Controls
- State Management
- Device Features (camera, geolocation, etc.)
Each of these areas benefits from the encapsulation and reusability that custom hooks provide.
8. Future of Custom Hooks
As React continues to evolve, custom hooks are becoming increasingly important in modern React development. They're not just a pattern but a fundamental building block for sharing logic in React applications. Keep an eye on:
- Integration with React Server Components
- New patterns emerging from the community
- Performance optimizations in React 19
- Integration with other React features like Suspense
Remember, the best custom hooks are those that solve specific problems while remaining flexible enough to be used in various situations. Start simple, and add complexity only when needed.