# Effects and Refs

# Rules of hooks

Hooks (useState, useEffect, useRef, custom use* functions) may only be called:

  • at the top level of a function component or custom hook—never inside if, loops, or nested functions;
  • in the same order every render (which the top-level rule guarantees).

React matches hook calls to their state by call order, so conditional hooks corrupt the component's state.

# What effects are for

useEffect synchronizes a component with an external system: a network subscription, a timer, a DOM API, a third-party widget. Do not use effects for:

  • derived values—compute them during render (const visible = items.filter(...));
  • responding to user events—that logic belongs in the event handler.
import { useEffect, useState } from 'react';

function ElapsedCounter() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(id);
  }, []);

  return <p>{seconds} s elapsed</p>;
}

The dependency array lists the reactive values the effect uses—props and state that can change between renders. Module constants and state setters are stable and are excluded by convention. With [] the effect runs after the component's first commit and its cleanup runs on unmount; in development, React 18 StrictMode additionally runs setup → cleanup → setup once on mount to expose missing cleanup. That extra cycle is dev-only and does not happen in production builds. The cleanup also runs before each re-run when dependencies change.

# Fetching data

The effect callback itself cannot be async (it would return a Promise instead of a cleanup function); define an async function inside it. Guard the request with AbortController, check response.ok, and validate the unknown JSON before trusting its shape. The example assumes an endpoint /api/products that returns Product[] JSON—that contract is illustrative, not implemented by these snippets; adapt the URL and validation to your backend.

import * as React from 'react';

interface Product {
  id: number;
  name: string;
  price: number;
}

function isProduct(value: unknown): value is Product {
  if (typeof value !== 'object' || value === null) {
    return false;
  }
  const p = value as Record<string, unknown>;
  return typeof p.id === 'number' && Number.isFinite(p.id)
    && typeof p.name === 'string'
    && typeof p.price === 'number' && Number.isFinite(p.price);
}

export default function ProductList() {
  const [products, setProducts] = React.useState<Product[]>([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState<string | null>(null);

  React.useEffect(() => {
    const controller = new AbortController();

    const load = async () => {
      try {
        const response = await fetch('/api/products', {
          signal: controller.signal,
        });
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const data: unknown = await response.json();
        if (!Array.isArray(data) || !data.every(isProduct)) {
          throw new Error('Unexpected response shape');
        }
        if (!controller.signal.aborted) {
          setProducts(data);
        }
      } catch (e) {
        if (!controller.signal.aborted) {
          setError(e instanceof Error ? e.message : 'Failed to load');
        }
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    load();
    return () => controller.abort();
  }, []);

  if (loading) return <p>Loading…</p>;
  if (error) return <p role="alert">{error}</p>;
  if (products.length === 0) return <p>No products yet.</p>;

  return (
    <ul>
      {products.map((p) => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}

Note all four UI states—loading, error, empty, success—and that cleanup plus the aborted guards make the component abandon obsolete request results rather than blindly setting state.

A tempting shorthand is writing React.useEffect(async () => { ... }) directly. That does not work: an async function returns a Promise, but React expects the effect callback to return nothing or a cleanup function. The inner-async-function pattern above is the fix.

# Refs for DOM access

useRef returns a mutable { current } box. Changing current does not re-render—use refs for DOM handles, timers, or previous values, not for data the UI displays (that is state).

import { useRef } from 'react';

function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  const focusInput = () => {
    inputRef.current?.focus();
  };

  return (
    <>
      <label>
        Search:
        <input ref={inputRef} type="search" />
      </label>
      <button type="button" onClick={focusInput}>Focus</button>
    </>
  );
}

current is null until React attaches the element, so always guard with ?. or an if before dereferencing. Initializing with useRef<HTMLInputElement>(null) also gives the ref the RefObject<HTMLInputElement> shape React expects for element refs; calling useRef<HTMLInputElement>() with no argument produces a different type that won't assign cleanly to ref.

# Class lifecycle equivalents

Class components use lifecycle methods where function components use effects—but this is a rough comparison, not exact equivalents. Effects run after the commit to the DOM and their cleanup is tied to their own dependencies, not to a one-to-one lifecycle mapping:

Class method Roughly comparable hook usage
componentDidMount useEffect(fn, []) setup
componentWillUnmount the cleanup return
componentDidUpdate useEffect re-running when listed dependencies change

On a class component you would add logging like this to observe the ordering:

componentDidMount(): void {
  console.log('mounted');
}

componentWillUnmount(): void {
  console.log('unmounted');
}

Try it: add a cleanup that logs, mount and unmount the component (e.g. toggle {show && <ProductList />}), and observe the order of log lines. Then enable React.StrictMode and explain why the effect runs its setup and cleanup an extra time on mount in development.