Hook – 5:03 AM, Lisbon ↔ Tokyo code review
Coffee barely brewed when my Slack lit up: “James, why does every component in the dashboard re-fetch on scroll?” Kenji in Tokyo had copy-pasted the sameuseEffect
block into eight files—and they were fighting for bandwidth like seagulls over fries. We screen-shared; within twenty minutes we replaced the spaghetti with a single custom hook calleduseInfiniteScroll
. Network waterfall plummeted, Kenji’s face un-scrunched, and my espresso stayed hot. That remote epiphany is today’s agenda: I’ll show you how and when to craft your own hooks in React 18, turning duplicated logic into reusable, testable super-powers—no matter where your teammates sit on the globe.
Why Custom Hooks Matter in 2025
React’s built-in hooks—useState
, useEffect
, useMemo
—solve 80 % of state and side-effect needs. The remaining 20 %? That’s where codebases rot or shine. Copy-pasted fetch
blocks, duplicated event listeners, and ad-hoc debouncers inflate bundle size and sap maintainability. React 18 raises the stakes:
Pain Point | Why It Hurts Today | Custom Hook Advantage |
---|---|---|
Repeated data-fetch code across components | Wastes bandwidth, breaks DRY | useFetch() centralizes caching & errors |
Complicated effect cleanup | Memory leaks in concurrent renders | Encapsulate cleanup inside a hook |
Inconsistent accessibility | ARIA attributes forgotten | Hook returns props with ARIA baked in |
Companies migrating to React 18 ask juniors to “write a hook for that.” Mastering the pattern boosts your résumé and your team’s lighthouse score.
The Anatomy of a Custom Hook
Plain English: A custom hook is a JavaScript function whose name starts with use
and itself calls other hooks. It returns any value—state, callbacks, JSX props—so calling components stay lean.
Guiding Principles
- Extract Repetition. See the same 5-line side-effect thrice? Hook it.
- Isolate Side-Effects. Keep your markup pure; push browser APIs into hooks.
- Return the Minimal API. Expose only what the caller needs (state + handlers).
Hands-On: Building useLocalStorageState
1 — Boilerplate Function
jsxCopyEdit// hooks/useLocalStorageState.js
import { useState, useEffect } from 'react';
export function useLocalStorageState(key, initial) {
const [value, setValue] = useState(() => {
const cached = window.localStorage.getItem(key);
return cached !== null ? JSON.parse(cached) : initial;
});
// sync to localStorage
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
Line-by-line:
- Lazy initializer pulls from storage only once—crucial for React 18 strict-mode double-mount during development.
- Effect serializes on every change; stringify guards against objects.
2 — Consuming the Hook
jsxCopyEditimport { useLocalStorageState } from './hooks/useLocalStorageState';
export default function ThemeToggle() {
const [dark, setDark] = useLocalStorageState('dark', false);
return (
<button onClick={() => setDark(!dark)} aria-pressed={dark}>
{dark ? '🌙 Dark' : '☀️ Light'}
</button>
);
}
No storage boilerplate in the component—just intent.
3 — Common Pitfalls
Bug | Symptom | Fix |
---|---|---|
SSR mismatch | window undefined on server | Guard with typeof window !== "undefined" |
Circular JSON | setValue stores functions | Validate before JSON.stringify |
Key collisions | Different hooks share key | Namespace keys per feature |
Advanced Example: useIntersectionObserver
(Infinite Scroll)
jsxCopyEditimport { useEffect, useRef, useState } from 'react';
export function useIntersectionObserver(cb, options) {
const ref = useRef(null);
const [isIntersecting, setIntersecting] = useState(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
setIntersecting(entry.isIntersecting);
if (entry.isIntersecting) cb(); // call user callback
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [cb, options]);
return [ref, isIntersecting];
}
Usage:
jsxCopyEditconst [anchorRef] = useIntersectionObserver(fetchNextPage, { rootMargin: '200px' });
return (
<>
{/* list items */}
<div ref={anchorRef} />
</>
);
React 18’s automatic batching ensures state (isIntersecting
) updates without multiple paints when cb
triggers its own setState
.
Remote-Work Insight 📡
Sidebar (≈130 words)
During a sprint retro between Nairobi and New York, we realized each office wrote its ownuseDebounce
—five slight variations! The fix: spin up a shared hooks package published to our private npm registry. We added Storybook stories so design could test behaviours without spinning a full app, and set up Renovate to bump versions. Time-zones no longer mattered—everyone imports the same battle-tested hook.
Performance & Accessibility Checkpoints
- Lighthouse: Custom hooks often wrap expensive observers; throttle CPU and ensure FPS stays >55.
- React Profiler: Verify memoization—if your hook returns objects, wrap them in
useMemo
to avoid prop-drilling re-renders. - ARIA: If a hook manipulates focus or announces loading, return ready-made ARIA props so consumers can spread them on elements.
Helpful Command & Concept Table
Tool / Concept | One-liner purpose |
---|---|
npm init @eslint/config | Enforce hook exhaustive-deps rule |
useSyncExternalStore | Stable subscription hook underpinning libraries |
react-hooks/exhaustive-deps | ESLint rule to avoid stale closures |
npm publish --access restricted | Share hooks package within org |
CLI Quick-Start
Command | What it does |
---|---|
npm i react@18 react-dom@18 | Installs React 18 |
npx create-vite@latest my-hooks --template react | Boilerplate with SWC |
npm i -D eslint-plugin-react-hooks | Lint hook misuse |
Wrap-Up
Custom hooks convert scattered logic into composable bricks—crucial when teams sprawl across continents. Build small (useLocalStorageState
), scale to advanced (useIntersectionObserver
), and document like a library author. In React 18 land, where concurrency and batching reign, well-designed hooks ensure every user—on 5G or café Wi-Fi—gets a glitch-free ride. Share your favourite custom hooks or war stories below; I reply between kite-surf sessions and code reviews.