Why? Separates behavior from presentation and encourages composition.
Copy‑pasting the same useEffect
, event listeners, or form state across components clutters files and invites bugs when one copy drifts. Custom Hooks let you package that behavior once and share it everywhere—without tying it to any specific UI. Beginners sometimes build a “hook” that actually returns JSX or leave the logic inline, missing out on composition and testability.
// useWindowSize.ts
import { useEffect, useState } from "react";
export function useWindowSize() {
const [size, setSize] = useState<[number, number]>([
window.innerWidth,
window.innerHeight,
]);
useEffect(() => {
const onResize = () => setSize([window.innerWidth, window.innerHeight]);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return size; // <-- data only, no JSX!
}
// ResponsiveCard.tsx
import { useWindowSize } from "./useWindowSize";
export default function ResponsiveCard({
children,
}: {
children: React.ReactNode;
}) {
const [w] = useWindowSize();
const isCompact = w < 640;
return (
<div className={isCompact ? "p-4" : "p-8 shadow-lg rounded-2xl"}>
{children}
</div>
);
}
What’s happening?
useWindowSize
holds all resize logic, returns only numbers.// "Hook" that mixes UI with logic — hard to reuse.
function useWindowSizeComponent() {
const [size, setSize] = useState<[number, number]>([
window.innerWidth,
window.innerHeight,
]);
useEffect(() => {
const onResize = () => setSize([window.innerWidth, window.innerHeight]);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
// ❌ Returning JSX breaks single‑responsibility
return (
<p className="text-xs text-stone-500">
Window: {size[0]}×{size[1]}
</p>
);
}
// Every consumer ends up stuck with this paragraph—or re‑implements the logic.
// Logic duplicated inline in multiple components.
function HeroBanner() {
const [w] = useState(window.innerWidth); // ❌ duplicate state
useEffect(() => {
const onResize = () => setW(window.innerWidth); // ❌ duplicate listener
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
/* render … */
}
use
so ESLint/react‑hooks can verify correct usage.