Custom Hooks
Custom Hooks are the crown jewel of React’s composition model. They allow you to extract component logic into reusable functions. If you find yourself copying and pasting useEffect code between components, you likely need a Custom Hook.
The Rules of Custom Hooks
- Naming: Must start with
use(e.g.,useWindowSize). This tells React to check for violations of the Rules of Hooks. - Usage: Can call other hooks (
useState,useEffect, etc.). - Independence: Two components using the same hook do NOT share state. Each gets its own isolated state instance.
Pattern 1: Logic Extraction (Before vs After)
Imagine you have a component that tracks the window size.
Before: Bloated Component
function App() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Width: {windowSize.width}px</div>;
}
After: Clean Component with useWindowSize
We extract the state and effect into a separate function.
// The Custom Hook
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({ width: window.innerWidth, height: window.innerHeight });
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
// The Component (Now Trivial)
function App() {
const size = useWindowSize();
return <div>Width: {size.width}px</div>;
}
Pattern 2: Synchronizing with Browser APIs
A classic use case is syncing state with browser APIs like localStorage or window.
useLocalStorage
function useLocalStorage(key, initialValue) {
// 1. Initialize state from local storage or default
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
// 2. Return a wrapped version of useState's setter function
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
Interactive: Hook Composition Demo
See how multiple small hooks combine to power a complex UI. We will compose a simulated useMousePosition hook and a useWindowSize hook to create a responsive, interactive tracking component.
Active Hooks
const { x, y } = useMousePosition();
const { width, height } = useWindowSize();
// Combined Result
return <div>
X: {x}, Y: {y}
Window: {width}x{height}
</div>
Mouse: 0, 0
Window: 0 x 0
Move mouse over this box!
Visualizing Logic Extraction
This diagram shows how we move logic from the Component into a Custom Hook.
graph LR
subgraph Component ["Component (Before)"]
A[State: user]
B[Effect: Fetch API]
C[Render: User Name]
A --- B
B --- C
end
subgraph Refactor ["Refactoring"]
D[Extract State & Effect] --> E((useUser))
end
subgraph CleanComponent ["Component (After)"]
F[Call: useUser()]
G[Render: User Name]
F --- G
end
style Component fill:var(--accent-main),stroke:#fff,color:#fff,fill-opacity:0.2
style CleanComponent fill:var(--green-500),stroke:#fff,color:#fff,fill-opacity:0.2
style E fill:var(--purple-500),stroke:#fff,color:#000
Key Takeaways
- DRY (Don’t Repeat Yourself): If you copy-paste logic, make a hook.
- Encapsulation: Hide complex implementation details (like
AbortController) inside the hook. - Composition: Hooks can use other hooks, allowing you to build complex logic from simple blocks.