HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
6 Topics
Chapter•100 Minutes

Hooks, Effects & External Systems

Learn the Rules of Hooks, effect lifecycle, dependency reasoning, refs, memoization hooks, custom hooks, and how to avoid effect-driven design problems.

Rules of Hooks

Hooks such as useState, useEffect, and useMemo must be called at the top level of React function components or custom hooks. Do not call hooks inside loops, conditions, nested functions, or arbitrary utility functions.

React relies on a consistent hook call order to associate hook state with a component render. Conditional hook calls change that order and break the association.

Custom hooks are functions whose names begin with use and that can call other hooks. They package reusable stateful behavior without sharing component state implicitly.

A custom hook
function useOnlineStatus() {
  const [online, setOnline] = useState(navigator.onLine);

  useEffect(() => {
    const onOnline = () => setOnline(true);
    const onOffline = () => setOnline(false);
    window.addEventListener("online", onOnline);
    window.addEventListener("offline", onOffline);
    return () => {
      window.removeEventListener("online", onOnline);
      window.removeEventListener("offline", onOffline);
    };
  }, []);

  return online;
}
💡 The hook owns the subscription lifecycle and exposes a small reusable API.
Key Takeaways
  • Hooks require stable call order.
  • Custom hooks share behavior, not state instances.
  • A custom hook should have a focused contract.

useEffect: Synchronizing with External Systems

An Effect is for synchronizing a component with something outside React: subscriptions, timers, browser APIs, imperative widgets, or network interactions when appropriate.

Do not use an Effect merely because a value can be calculated from other state. If fullName is derived from firstName and lastName, calculate it during render. An unnecessary effect creates an extra render and introduces synchronization complexity.

An effect has a setup phase and optionally a cleanup phase. Cleanup should undo the external subscription, timer, listener, or resource established by the setup.

Effect Lifecycle
Subscription cleanup
useEffect(() => {
  const connection = connect(roomId);
  connection.on("message", handleMessage);

  return () => {
    connection.off("message", handleMessage);
    connection.disconnect();
  };
}, [roomId, handleMessage]);
💡 When the room changes or the component unmounts, cleanup prevents the previous connection from remaining active.
Key Takeaways
  • Effects synchronize with external systems.
  • Cleanup must reverse setup work.
  • Do not use effects for ordinary derivations.
Knowledge Check

Which is the best reason to use `useEffect`?

Effect Dependencies & Stale Closures

The dependency array describes reactive values used by the effect. If an effect reads a prop or state value, that value generally belongs in its dependency list unless the design intentionally uses another mechanism.

A stale closure occurs when a callback retains values from an older render. This commonly appears with timers, subscriptions, or asynchronous callbacks. The solution is not always to add an empty dependency array. Instead, decide what the effect should synchronize and ensure the callback sees the intended values.

The exhaustive-deps lint rule is useful because missing dependencies often indicate a design problem. When satisfying it causes a loop, reconsider the effect rather than suppressing the warning blindly.

Empty dependency arrays are not a lifecycle command

[] means the effect does not depend on reactive values from the component. It should not be used as a general-purpose 'run once' escape hatch.

Key Takeaways
  • Dependencies describe reactive inputs to an effect.
  • Stale closures are often symptoms of unclear synchronization design.
  • Avoid suppressing dependency warnings without understanding the lifecycle.

useRef: Mutable Values & DOM References

useRef stores a mutable value that persists across renders without causing a re-render when changed. It is also commonly used to reference DOM nodes.

Use refs for values that do not belong in the rendered UI: timer IDs, imperative handles, previous values, or DOM nodes. If changing the value should update the screen, it usually belongs in state instead.

A ref's .current is mutable. Mutating it during render can make code difficult to reason about and should generally be avoided unless you have a very specific, well-understood pattern.

DOM ref
function SearchInput() {
  const inputRef = useRef(null);

  function focusInput() {
    inputRef.current?.focus();
  }

  return (
    <>
      <input ref={inputRef} />
      <button onClick={focusInput}>Focus</button>
    </>
  );
}
💡 The ref provides an imperative escape hatch for a DOM operation without storing the element in state.
Key Takeaways
  • Refs persist values without triggering renders.
  • Use state for values that affect rendered output.
  • Refs are useful for DOM access and imperative integrations.

useMemo, useCallback & React.memo

Memoization can avoid repeated work or preserve referential identity, but it is not a default requirement. First make the code correct; then profile actual bottlenecks.

useMemo caches a calculated value. useCallback caches a function identity. React.memo can skip rendering a component when its props are considered equal. These techniques work together when a stable prop identity enables a child to skip expensive work.

Memoization can also make code harder to understand and consume memory. A memoized computation that is cheaper than maintaining the cache is often a net loss.

Memoizing expensive derived data
const visibleProducts = useMemo(
  () => products.filter(p => matches(p, query)),
  [products, query]
);
💡 Memoization is appropriate when filtering is measurably expensive or when stable identity matters to downstream memoized components.
Key Takeaways
  • Memoization is an optimization, not a correctness mechanism.
  • Profile before adding complex memoization.
  • Stable identity matters when consumers compare references.

Designing High-Quality Custom Hooks

A custom hook should hide implementation details while exposing a small, predictable API. For example, useDebouncedValue(value, delay) should not force callers to understand timers, cleanup, or dependency management.

Good hooks separate reusable behavior from UI. A hook may return state, actions, status flags, and refs. Avoid hooks that return a huge object containing every internal detail of a feature.

Custom hooks can compose other hooks. They are a major reason React's function-component model scales beyond simple components.

Debounced value hook
function useDebouncedValue<T>(value: T, delay: number) {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}
💡 The hook owns the timer lifecycle and exposes only the debounced value.
Key Takeaways
  • Custom hooks should expose behavior, not implementation.
  • Keep hook APIs small and predictable.
  • Hooks can compose state, effects, refs, and other hooks.