Custom hooks are the key to reusing stateful logic in React applications. They let you extract component logic into reusable functions, following the DRY principle and making your codebase more maintainable. Let's master the art of creating custom hooks.
What Are Custom Hooks?
Custom hooks are JavaScript functions that start with "use" and can call other hooks. They allow you to reuse stateful logic between components without changing your component hierarchy.
The Power of Abstraction
Custom hooks abstract complex logic behind simple interfaces. Components can use this logic without understanding the implementation details, similar to how built-in hooks work.
// Custom hook
function useWindowSize() {
const [size, setSize] = useState({ width: 0, height: 0 });
// Complex window resize logic
return size;
}
// Component usage
function MyComponent() {
const { width, height } = useWindowSize();
return <div>Window: {width}x{height}</div>;
}Naming Conventions
Always prefix custom hooks with "use". This is important for several reasons: it clearly indicates the hook uses other hooks, it enables ESLint's exhaustive-deps rule, and it follows React conventions.
// ✅ Good: Follows convention
function useLocalStorage(key, initialValue) { }
// ❌ Bad: Violates convention
function getLocalStorage(key, initialValue) { }Building Your First Custom Hook
Let's create a simple hook for managing localStorage with state synchronization.
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
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] as const;
}This hook combines useState with localStorage, providing a seamless state management experience that persists across page reloads.
Hooks for Data Fetching
Data fetching is a perfect use case for custom hooks. Extract common patterns into reusable hooks.
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) throw new Error('Network error');
const json = await response.json();
setData(json);
setError(null);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}
fetchData();
return () => abortController.abort();
}, [url]);
return { data, error, loading };
}Hooks for Form Handling
Forms have repetitive logic for managing values, validation, and submission. Custom hooks can centralize this logic.
function useForm<T extends Record<string, any>>(
initialValues: T,
validate?: (values: T) => Record<keyof T, string>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<Record<keyof T, string>>({} as any);
const [touched, setTouched] = useState<Record<keyof T, boolean>>({} as any);
const handleChange = (name: keyof T) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
setValues(prev => ({ ...prev, [name]: e.target.value }));
};
const handleBlur = (name: keyof T) => () => {
setTouched(prev => ({ ...prev, [name]: true }));
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
}
};
const handleSubmit = (callback: (values: T) => void) => (
e: React.FormEvent
) => {
e.preventDefault();
if (validate) {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
callback(values);
}
} else {
callback(values);
}
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
handleSubmit
};
}Hooks for Browser APIs
Custom hooks excel at wrapping browser APIs with React's lifecycle.
function useGeolocation() {
const [state, setState] = useState({
loading: true,
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: null,
longitude: null,
speed: null,
error: null
});
useEffect(() => {
const onSuccess = (position: GeolocationPosition) => {
setState({
loading: false,
accuracy: position.coords.accuracy,
altitude: position.coords.altitude,
altitudeAccuracy: position.coords.altitudeAccuracy,
heading: position.coords.heading,
latitude: position.coords.latitude,
longitude: position.coords.longitude,
speed: position.coords.speed,
error: null
});
};
const onError = (error: GeolocationPositionError) => {
setState(prev => ({
...prev,
loading: false,
error: error.message
}));
};
navigator.geolocation.getCurrentPosition(onSuccess, onError);
const watchId = navigator.geolocation.watchPosition(
onSuccess,
onError
);
return () => navigator.geolocation.clearWatch(watchId);
}, []);
return state;
}Hooks with Event Listeners
Managing event listeners is another great use case for custom hooks.
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: HTMLElement
) {
const savedHandler = useRef(handler);
useEffect(() => {
savedHandler.current = handler;
}, [handler]);
useEffect(() => {
const targetElement = element || window;
const isSupported = targetElement.addEventListener;
if (!isSupported) return;
const eventListener = (event: Event) =>
savedHandler.current(event as WindowEventMap[K]);
targetElement.addEventListener(eventName, eventListener);
return () => {
targetElement.removeEventListener(eventName, eventListener);
};
}, [eventName, element]);
}Composable Hooks
Hooks are most powerful when they compose with each other. Build complex behavior by combining simple hooks.
function useOnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEventListener('online', () => setOnline(true));
useEventListener('offline', () => setOnline(false));
return online;
}
function useNetworkStatus() {
const online = useOnlineStatus();
const [connectionType, setConnectionType] = useState<ConnectionType | null>(null);
useEffect(() => {
if ('connection' in navigator) {
const conn = (navigator as any).connection;
setConnectionType(conn.effectiveType);
const handleChange = () => setConnectionType(conn.effectiveType);
conn.addEventListener('change', handleChange);
return () => conn.removeEventListener('change', handleChange);
}
}, []);
return { online, connectionType };
}Hooks for Animation
Animation hooks abstract complex timing and state management.
function useCounter(duration: number) {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(c => c + 1);
}, duration);
return count;
}
function useInterval(callback: () => void, delay: number) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
const tick = () => savedCallback.current();
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}Parameterized Hooks
Create flexible hooks by accepting configuration parameters.
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(v => !v), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse, setValue };
}Testing Custom Hooks
Test hooks in isolation using @testing-library/react-hooks.
import { renderHook, act } from '@testing-library/react-hooks';
test('useCounter increments and decrements', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});Best Practices
- 1Keep hooks focused on a single responsibility
- 2Document hook parameters and return values
- 3Use TypeScript for type safety
- 4Test hooks in isolation
- 5Handle cleanup properly in useEffect
- 6Use useCallback for functions returned from hooks
- 7Compose simple hooks to build complex behavior
- 8Name hooks descriptively based on their purpose
Common Mistakes to Avoid
Don't call hooks inside loops or conditions. Don't call hooks from regular JavaScript functions. Don't forget to include all dependencies in useEffect. Don't create hooks that are too broad or do too much. Don't forget to handle cleanup and error cases.
Conclusion
Custom hooks are React's abstraction mechanism for reusing stateful logic. They let you extract complex logic from components into reusable, testable functions. By composing hooks together, you can build sophisticated features from simple building blocks.
Start by identifying repetitive logic in your components, extract it into a custom hook, and watch your codebase become more maintainable. Remember: the best hooks are small, focused, and composable.
Master custom hooks, and you'll write React code that's cleaner, more reusable, and easier to maintain. They're one of React's most powerful features for building scalable applications.