Your Logic, Your Rules: The Power of Custom Hooks
In React, a **Custom Hook** is more than just a function. It's a convention that allows you to extract and reuse **stateful logic** from a component. If you find yourself copying and pasting the same `useState` and `useEffect` logic across multiple components, you've found a perfect candidate for a custom hook.
This pattern is the cornerstone of a clean, scalable, and maintainable React codebase. It follows the **DRY (Don't Repeat Yourself)** principle, allowing you to build complex UIs from simple, declarative, and testable building blocks.
The Two Rules You Must Follow
React relies on two simple rules to make Hooks work. Violating them will lead to confusing bugs and unexpected behavior.
- 1. Only Call Hooks at the Top Level: Do not call Hooks inside loops, conditions, or nested functions. This ensures that Hooks are called in the same order on every single render, which is how React preserves state between calls.
- 2. Only Call Hooks from React Functions: You can only call Hooks from a React functional component or from another custom hook. You cannot call them from regular JavaScript functions.
By convention, custom hooks must also have a name that starts with `use` (e.g., `useFetch`, `useToggle`). This allows React and linters to automatically check for violations of the Rules of Hooks.
Pattern 1: The `useToggle` Hook (Simple State)
The simplest custom hook often just abstracts a single `useState` call and its update logic. A common example is a boolean toggle.
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
// useCallback ensures the toggle function has a stable identity
const toggle = useCallback(() => {
setValue(prev => !prev);
}, []);
return [value, toggle];
}
// How to use it:
// const [isOn, toggleIsOn] = useToggle(false);This hook encapsulates the logic of "toggling a boolean." The component using it doesn't need to know *how* the toggle works; it just receives the current state (`value`) and the function to change it (`toggle`).
Pattern 2: The `useFetch` Hook (State + Effect)
A more powerful pattern involves combining `useState` with `useEffect` to manage side effects, like fetching data from an API.
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// AbortController handles component unmounting mid-fetch
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
})
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
setError(err);
}
})
.finally(() => setLoading(false));
// Cleanup function: Abort fetch if component unmounts
return () => controller.abort();
}, [url]); // Re-run the effect if the URL changes
return { data, loading, error };
}This hook is a complete data-fetching solution. It handles loading state, error state, and even cleans up after itself to prevent memory leaks. Any component can now fetch data with a single line: const { data, loading, error } = useFetch('/api/my-data');
Pattern 3: The `useWindowSize` Hook (Effect with Cleanup)
Custom hooks are perfect for abstracting browser APIs. This hook listens to the window's `resize` event and provides the current dimensions.
import { useState, useEffect } from 'react';
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
// Crucial cleanup: remove the listener when the hook unmounts
return () => window.removeEventListener('resize', handleResize);
}, []); // Empty array means this effect runs only once (on mount)
return size;
}
// How to use it:
// const { width, height } = useWindowSize();The most important part of this hook is the **cleanup function** returned from `useEffect`. Without it, you would add a new event listener every time the component mounts, creating a massive memory leak.
Key Takeaway: Custom Hooks transform your components from complex, state-managing monoliths into clean, declarative functions that simply consume logic. This makes your code easier to read, test, and maintain.