Custom hooks are the secret sauce of React development. They allow you to extract component logic into reusable functions, keeping your components lean and your codebase DRY (Don't Repeat Yourself).
Think of them as a way to "package" complex stateful logic so you can use it anywhere in your app without copy-pasting code.
❓What is a Custom Hook?
A custom hook is a JavaScript function whose name starts with "use" and that may call other hooks.
The Goal: Share logic between components, not state itself. Each call to a custom hook gets a completely isolated state.
The Requirement: You must follow the Rules of Hooks (e.g., only call hooks at the top level).
Why use them?
Readability: Hide complex logic behind a descriptive name like
useAuthoruseFetch.Reusability: Write once, use in multiple components.
Testability: You can test the logic of the hook independently of the UI.
👀 Anatomy of a Custom Hook
A custom hook typically follows this pattern:
Define the function (starting with
use).Use built-in hooks (
useState,useEffect, etc.) inside.Return what the component needs (values, functions, or objects).
Example: useWindowWidth
Suppose you want to track the browser window size in multiple components.
import { useState, useEffect } from 'react';
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup the listener on unmount
return () => window.removeEventListener('resize', handleResize);
}, []);
return width;
}🧠 Code Walkthrough
1. Initializing State
const [width, setWidth] = useState(window.innerWidth);We initialize the state with the actual current width of the window. This ensures that the very first time a component uses this hook, it has a real value to display rather than null or 0.
2. The Side Effect (useEffect)
The "magic" happens inside useEffect. We need to tell the browser: "Hey, every time the window changes size, let me know."
The Listener:
const handleResize = () => setWidth(window.innerWidth);This function runs every time the event triggers, updating our React state with the new width.
The Attachment:
window.addEventListener('resize', handleResize);This hooks our function into the browser's native resize event.
The Dependency Array
[]: By passing an empty array, we ensure this setup only happens once when the component (using the hook) mounts.
3. The Cleanup (Crucial!)
return () => window.removeEventListener('resize', handleResize);Custom hooks that use subscriptions or event listeners must clean up after themselves. If you don't remove the listener, it stays in the browser's memory even after the component is gone, leading to memory leaks and performance issues.
4. The Return Value
return width;The hook returns the raw number. The component using it doesn't care how the width is calculated; it just wants the number.
How to Use It in a Component
Once defined, using it is incredibly simple:
function MyResponsiveComponent() {
const width = useWindowWidth(); // Simple!
return (
<div>
<p>Window width is: {width}px</p>
{width < 600 ? <MobileView /> : <DesktopView />}
</div>
);
}<div class="infographic-windowwidth" id="ww">
<style>
.infographic-windowwidth {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--line: #ddd6c9;
--accent: #2563eb;
--radius: 8px;
--mono-font: 'IBM Plex Mono', monospace;
--body-font: 'Inter', system-ui, sans-serif;
max-width: 700px;
margin: 0 auto;
box-sizing: border-box;
background: transparent;
color: var(--ink);
font-family: var(--body-font);
}
.infographic-windowwidth[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--line: #2c313d;
--accent: #5b8def;
}
.infographic-windowwidth * { box-sizing: border-box; min-width: 0; }
.ww-grid { display: flex; flex-direction: column; gap: 1.1rem; align-items: stretch; }
.ww-output-label {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--ink-soft); text-align: center;
}
.ww-preview { border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; box-shadow: 0 10px 24px -12px rgba(26,26,26,0.18); }
.ww-preview-bar { background: #e9e9e9; padding: 0.45rem 0.6rem; display: flex; gap: 0.3rem; }
.ww-preview-bar span { width: 8px; height: 8px; border-radius: 50%; display: block; }
.ww-preview-bar span:nth-child(1) { background: #ff5f56; }
.ww-preview-bar span:nth-child(2) { background: #ffbd2e; }
.ww-preview-bar span:nth-child(3) { background: #27c93f; }
.ww-preview-page { background: #fff; padding: 1.6rem; text-align: center; color: #000; }
.ww-width-line { font-size: 0.95rem; margin: 0 0 1rem; }
.ww-view-banner {
font-weight: 700; font-size: 0.9rem; padding: 0.7rem; border-radius: 8px; margin: 0 0 1.1rem;
transition: background 0.25s ease, color 0.25s ease;
}
.ww-view-banner.mobile { background: #fde3e3; color: #b91c1c; }
.ww-view-banner.desktop { background: #dcf5e3; color: #15803d; }
.ww-slider-row { display: flex; flex-direction: column; gap: 0.4rem; align-items: center; }
.ww-slider-label { font-family: var(--mono-font); font-size: 0.7rem; color: #666; }
.ww-slider { width: 100%; max-width: 320px; accent-color: var(--accent); }
.ww-live-note { font-size: 0.72rem; color: #888; margin-top: 0.6rem; }
@media (prefers-reduced-motion: reduce) {
.infographic-windowwidth *, .infographic-windowwidth *::before, .infographic-windowwidth *::after {
transition-duration: 0.001ms !important; animation-duration: 0.001ms !important;
}
}
</style>
<div class="ww-grid">
<p class="ww-output-label">OUTPUT</p>
<div class="ww-preview">
<div class="ww-preview-bar"><span></span><span></span><span></span></div>
<div class="ww-preview-page">
<p class="ww-width-line">Window width is: <strong id="wwWidthVal">0</strong>px</p>
<div class="ww-view-banner" id="wwBanner">DesktopView</div>
<div class="ww-slider-row">
<span class="ww-slider-label">Drag to simulate a resize:</span>
<input class="ww-slider" type="range" id="wwSlider" min="300" max="1400" value="900" />
</div>
<p class="ww-live-note">Resizing your actual browser window updates this too.</p>
</div>
</div>
</div>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('ww');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'darkMode') {
if (e.data.value) root.setAttribute('data-theme', 'dark');
else root.removeAttribute('data-theme');
}
});
var widthEl = root.querySelector('#wwWidthVal');
var bannerEl = root.querySelector('#wwBanner');
var slider = root.querySelector('#wwSlider');
var usingSlider = false;
function applyWidth(w) {
widthEl.textContent = w;
if (w < 600) {
bannerEl.className = 'ww-view-banner mobile';
bannerEl.textContent = '📱 MobileView';
} else {
bannerEl.className = 'ww-view-banner desktop';
bannerEl.textContent = '🖥️ DesktopView';
}
}
// real behavior: the same window.addEventListener('resize', ...) from the code
window.addEventListener('resize', function () {
if (usingSlider) return; // don't fight the slider while it's being dragged
applyWidth(window.innerWidth);
slider.value = Math.min(1400, Math.max(300, window.innerWidth));
});
slider.addEventListener('input', function () {
usingSlider = true;
applyWidth(parseInt(slider.value, 10));
});
slider.addEventListener('change', function () { usingSlider = false; });
// initial render — same as useState(window.innerWidth)
applyWidth(window.innerWidth);
slider.value = Math.min(1400, Math.max(300, window.innerWidth));
})();
</script>✏️ Implementation Workflow
When building your own, follow these steps:
Step | Action | Why? |
|---|---|---|
1. Identify | Find logic that repeats or is too bulky for a component. | Keeps components focused on UI. |
2. Extract | Move that logic into a new function named | "use" prefix lets React check for hook rule violations. |
3. Generalize | Use arguments to make the hook flexible (e.g., passing a URL to a fetch hook). | Increases reusability. |
4. Return | Decide if you need to return an array | Arrays are good for renaming; objects are better for many values. |
✅ Common Patterns & Best Practices
Return Objects for Clarity: If your hook returns more than two values, use an object. This prevents users from having to remember the exact order of elements in an array.
Keep it Pure-ish: Hooks should manage logic, not side effects that affect the global window unless necessary.
Compose Hooks: You can call custom hooks inside other custom hooks! This allows for building highly specialized logic from small, building-block hooks.
↔️ Quick Comparison: Component vs. Hook
Feature | Component | Custom Hook |
|---|---|---|
Purpose | Returns UI (JSX) | Returns Data/Logic |
Naming | PascalCase ( | camelCase with "use" ( |
State | Holds UI state | Manages logic state |
Note: Remember that custom hooks don't share state. If ComponentA and ComponentB both use useCounter, they each get their own independent count variable.