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 same useEffect 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 called useInfiniteScroll. 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 PointWhy It Hurts TodayCustom Hook Advantage
Repeated data-fetch code across componentsWastes bandwidth, breaks DRYuseFetch() centralizes caching & errors
Complicated effect cleanupMemory leaks in concurrent rendersEncapsulate cleanup inside a hook
Inconsistent accessibilityARIA attributes forgottenHook 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

  1. Extract Repetition. See the same 5-line side-effect thrice? Hook it.
  2. Isolate Side-Effects. Keep your markup pure; push browser APIs into hooks.
  3. 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:

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

BugSymptomFix
SSR mismatchwindow undefined on serverGuard with typeof window !== "undefined"
Circular JSONsetValue stores functionsValidate before JSON.stringify
Key collisionsDifferent hooks share keyNamespace 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 own useDebounce—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


Helpful Command & Concept Table

Tool / ConceptOne-liner purpose
npm init @eslint/configEnforce hook exhaustive-deps rule
useSyncExternalStoreStable subscription hook underpinning libraries
react-hooks/exhaustive-depsESLint rule to avoid stale closures
npm publish --access restrictedShare hooks package within org

CLI Quick-Start

CommandWhat it does
npm i react@18 react-dom@18Installs React 18
npx create-vite@latest my-hooks --template reactBoilerplate with SWC
npm i -D eslint-plugin-react-hooksLint 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.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x