Effect Isolation

Use `useEffect` (or `useLayoutEffect` only when layout‑critical) solely for side‑effects; keep calculations inside render or `useMemo`.

Why?  Prevents unnecessary asynchronous work and avoids tearing.

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.

Correct Example

// 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.
  • Any component can now respond to window size without duplicating listeners or DOM code.

Incorrect Example

// "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 … */
}

Key Takeaways

  • Custom Hook = behavior package: Encapsulate state + side effects, return data and callbacks—never JSX.
  • Reuse beats copy‑paste: Centralizing logic avoids drift and simplifies bug fixes.
  • Composable building blocks: Hooks can call other hooks, letting you layer behavior without giant components.
  • Test in isolation: Logic‑only hooks can be unit‑tested without rendering DOM.
  • Naming convention: Always start with use so ESLint/react‑hooks can verify correct usage.