React Custom Hooks
Custom hooks are a powerful feature in React that lets you extract component logic into reusable functions. They follow the same rules as built-in hooks and give you a way to share stateful behavior between components without changing your component hierarchy.
If you’re new to hooks, you might want to first learn about the useState hook and useEffect hook.
What Makes a Custom Hook?
A custom hook is just a JavaScript function whose name starts with “use” and that can call other hooks. That’s it! The “use” convention is important because it lets React automatically check for violations of the rules of hooks.
// This is a custom hook
function useMyCustomLogic() {
const [value, setValue] = useState(0);
// Your custom logic here
return value;
}Creating Your First Custom Hook
Let’s create a simple custom hook that manages a counter. This demonstrates how to extract state logic into a reusable function.
// useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(count + 1);
const decrement = () => setCount(count - 1);
const reset = () => setCount(initialValue);
return {
count,
increment,
decrement,
reset
};
}
export default useCounter;Now you can use this hook in any component:
// CounterComponent.jsx
import useCounter from './useCounter';
function CounterComponent() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<h2>Count: {count}</h2>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}Real-World Example: API Data Fetching
One of the most common uses for custom hooks is fetching data from APIs. Here’s a comprehensive data fetching hook:
// useApi.js
import { useState, useEffect } from 'react';
function useApi(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (isMounted) {
setData(result);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
};
if (url) {
fetchData();
}
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}
export default useApi;Using this hook in a component:
// UserComponent.jsx
import useApi from './useApi';
function UserComponent({ userId }) {
const { data: user, loading, error } = useApi(
userId ? `https://jsonplaceholder.typicode.com/users/${userId}` : null
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>Select a user</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
</div>
);
}Custom Hook for Local Storage
Here’s a hook that synchronizes state with localStorage:
// useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get from local storage then parse stored json or return 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;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value) => {
try {
// Allow value to be a function so we have same API as useState
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];
}
export default useLocalStorage;Using the localStorage hook:
// SettingsComponent.jsx
import useLocalStorage from './useLocalStorage';
function SettingsComponent() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 'medium');
return (
<div>
<h2>Settings</h2>
<div>
<label>
Theme:
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</label>
</div>
<div>
<label>
Font Size:
<select value={fontSize} onChange={(e) => setFontSize(e.target.value)}>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</label>
</div>
<p>Current theme: {theme}</p>
<p>Current font size: {fontSize}</p>
</div>
);
}Debounced Input Hook
This hook is useful for search inputs where you want to wait until the user stops typing:
// useDebounce.js
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;
}
export default useDebounce;Combining with the API hook for a search component:
// SearchComponent.jsx
import { useState } from 'react';
import useApi from './useApi';
import useDebounce from './useDebounce';
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const { data: results, loading, error } = useApi(
debouncedSearchTerm
? `https://jsonplaceholder.typicode.com/users?q=${debouncedSearchTerm}`
: null
);
return (
<div>
<h2>User Search</h2>
<input
type="text"
placeholder="Search users..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
{loading && <div>Searching...</div>}
{error && <div>Error: {error}</div>}
{results && (
<div>
<h3>Results:</h3>
<ul>
{results.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
)}
</div>
);
}Window Size Hook
This hook tracks the browser window size:
// useWindowSize.js
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: undefined,
height: undefined,
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
}
// Add event listener
window.addEventListener('resize', handleResize);
// Call handler right away so state gets updated with initial window size
handleResize();
// Remove event listener on cleanup
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
export default useWindowSize;Using it for responsive design:
// ResponsiveComponent.jsx
import useWindowSize from './useWindowSize';
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
<h2>Responsive Design</h2>
{width < 768 ? (
<div>Mobile layout (width: {width}px)</div>
) : width < 1024 ? (
<div>Tablet layout (width: {width}px)</div>
) : (
<div>Desktop layout (width: {width}px)</div>
)}
</div>
);
}Best Practices for Custom Hooks
- Follow naming conventions: Always start with “use”
- Keep them focused: Each hook should do one thing well
- Make them reusable: Design hooks to work in different contexts
- Handle edge cases: Consider loading states, errors, and cleanup
- Document your hooks: Explain what they do and how to use them
- Test your hooks: Write tests to ensure they work correctly
Testing Custom Hooks
You can test custom hooks using React Testing Library’s renderHook function:
// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
test('should reset to initial value', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.reset();
});
expect(result.current.count).toBe(5);
});When to Create Custom Hooks
Create custom hooks when you:
- Find yourself copying and pasting the same stateful logic between components
- Want to share stateful logic without changing component structure
- Need to abstract complex behavior into a simple interface
- Want to make your components more readable and focused
Custom hooks are a fundamental part of modern React development. They help you write cleaner, more reusable code and make your components easier to understand and maintain.
For more advanced React patterns, check out our guide on React Context API or learn about performance optimization.
The React documentation provides excellent examples and guidelines for building custom hooks, and the community has created many useful hooks you can explore on GitHub.