Custom Hooks
Custom Hooks
Custom hooks are a powerful feature in React that allow you to extract component logic into reusable functions. They enable you to share stateful logic between multiple components without changing your component hierarchy.
What are Custom Hooks?
Custom hooks are JavaScript functions whose names start with "use" and that may call other hooks. They follow the same rules as React's built-in hooks.
Why Use Custom Hooks?
- Reusability: Share logic between multiple components
- Separation of Concerns: Keep components clean and focused on rendering
- Testing: Easier to test logic in isolation
- Composition: Build complex functionality by combining simpler hooks
Creating Your First Custom Hook
Let's create a simple custom hook that manages a counter:
// 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;
Using the custom hook in a component:
import React from 'react';
import useCounter from './useCounter';
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>
);
}
Common Custom Hook Patterns
1. useLocalStorage
A hook that syncs state with localStorage:
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);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists the new value to localStorage
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
2. useFetch
A hook for data fetching:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const jsonData = await response.json();
setData(jsonData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
3. useToggle
A simple toggle hook:
import { useState, useCallback } from 'react';
function useToggle(initialState = false) {
const [state, setState] = useState(initialState);
const toggle = useCallback(() => setState(state => !state), []);
return [state, toggle];
}
4. useWindowSize
A hook that tracks window dimensions:
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,
});
}
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
Best Practices
1. Naming Convention
Always start custom hook names with "use":
// Good
useAuth, useLocalStorage, useWindowSize
// Bad
getAuth, authHook, storageHook
2. Keep Hooks Pure
Custom hooks should be pure functions that don't cause side effects outside of React's lifecycle:
// Good
function useTimer(initialTime) {
const [time, setTime] = useState(initialTime);
useEffect(() => {
const interval = setInterval(() => {
setTime(t => t - 1);
}, 1000);
return () => clearInterval(interval);
}, []);
return time;
}
// Bad - modifying external state
let globalTimer;
function useBadTimer() {
globalTimer = setInterval(() => {}, 1000); // Don't do this!
}
3. Return Consistent Values
Return an object or array with consistent structure:
// Good - consistent return structure
function useApi(url) {
const [state, setState] = useState({
data: null,
loading: true,
error: null
});
// ... fetch logic
return { data: state.data, loading: state.loading, error: state.error };
}
// Also good - using array for simple cases
function useToggle(initial) {
const [value, setValue] = useState(initial);
const toggle = () => setValue(!value);
return [value, toggle];
}
Advanced Custom Hooks
useDebounce
Delays updating a value until after a specified delay:
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;
}
// Usage
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search with debouncedSearchTerm
}
}, [debouncedSearchTerm]);
return (
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
);
}
usePrevious
Stores the previous value of a prop or state:
import { useRef, useEffect } from 'react';
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Component({ count }) {
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
</div>
);
}
Testing Custom Hooks
Custom hooks can be tested using the React Testing Library's renderHook
utility:
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('useCounter increments count', () => {
const { result } = renderHook(() => useCounter(0));
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Conclusion
Custom hooks are a powerful pattern in React that promote code reuse and better organization. They allow you to:
- Extract component logic into reusable functions
- Share stateful logic between components
- Keep components focused on rendering
- Create cleaner, more maintainable code
Start simple and build more complex hooks as you become comfortable with the pattern. Remember to follow React's rules of hooks and naming conventions to ensure your custom hooks work correctly.