Start with the value, not the hook

Hooks are tools for connecting a component to stateful behavior. They are not a checklist every component needs to complete. Before choosing one, ask where the value comes from, how long it should live, and what should cause it to change.

If a value can be calculated from props or existing state during render, calculate it. Storing derived values creates two sources of truth and adds an update path that can fall out of sync. A surprising amount of hook complexity disappears when the render function is allowed to do ordinary work.

useState: durable UI memory

Use state when a value must survive renders and changing it should produce a new render: an open panel, an edited field, or a user-selected mode. Keep related transitions together, but do not combine unrelated values merely to reduce the number of calls.

The common gotcha is reading state as though updates happen immediately. React schedules updates, and multiple updates can be batched. When the next value depends on the previous one, use the functional form: setCount(current => current + 1). That makes the dependency explicit and avoids stale closures.

State placement matters as much as state shape. Keep it near the components that need it. Lift it only when multiple branches must coordinate, and move it to the URL or server-state layer when its lifetime extends beyond the component tree.

Functional updates avoid stale state
function Counter() {
  const [count, setCount] = useState(0);

  function incrementTwice() {
    setCount(current => current + 1);
    setCount(current => current + 1);
  }

  return <button onClick={incrementTwice}>{count}</button>;
}

useEffect: synchronize with something external

An effect is appropriate when rendering must synchronize with a system React does not control: a browser API, event subscription, timer, analytics boundary, or imperative third-party widget. It is usually not the right tool for calculating values, responding to a button click, or copying props into state.

Effects run after render, so using one to derive state guarantees an extra render and creates a temporary inconsistent state. Handle user actions in the event that caused them. Calculate render data during render. Reserve effects for synchronization.

Every effect should have a clear setup and cleanup story. Missing dependencies produce stale values; unstable object or function dependencies can produce repeated work. Rather than suppressing the dependency rule, restructure the code so the dependency list honestly describes what the effect uses.

Synchronize with an external event source
function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine);

  useEffect(() => {
    const update = () => setIsOnline(navigator.onLine);

    window.addEventListener('online', update);
    window.addEventListener('offline', update);

    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);

  return isOnline;
}

useMemo and useCallback: performance contracts

Memoization is useful when you have measured expensive computation, when referential identity is required by a memoized child, or when a stable value is necessary for another hook. It should not be applied automatically to every object and function.

Both hooks add dependency management and retain values in memory. If the calculation is cheap or the consumer rerenders anyway, memoization can make the code harder to understand without improving the experience. Profile first, then optimize the boundary that is actually expensive.

A stable callback does not mean its captured values stay current. Its dependency list still defines the closure. This is the same stale-state problem in a more polished wrapper.

useRef: mutable values that do not render

Refs are appropriate for DOM nodes and mutable values that must survive renders without causing one: an interval identifier, a previous measurement, or an imperative library instance. Updating a ref is intentionally invisible to React.

That invisibility is also the gotcha. If the screen should change when the value changes, it belongs in state. Using refs to avoid renders can create UI that quietly disagrees with its underlying data.

Custom hooks: share behavior, not lifecycle tricks

Create a custom hook when multiple components share a stateful behavior or when one component needs a named boundary around a coherent concern. A good custom hook exposes a small domain-oriented interface and hides synchronization details.

Custom hooks do not create shared state by themselves; each call gets its own state unless the hook connects to an external store or context. They also do not relax the Rules of Hooks. Calls must remain unconditional and at the top level so React can preserve their order between renders.

The best hook is often the one you can explain in plain language. Names such as useOnlineStatus or useWindowSize describe the behavior they provide. Names such as useEffectOnce usually describe a lifecycle workaround rather than a reusable concern.

A small decision rule

Use state for information the interface remembers. Use effects to synchronize with systems outside React. Use memoization only for a measured performance or identity requirement. Use refs for persistent values that should not render. Use custom hooks to give shared stateful behavior a clear name.

When a component accumulates hooks, do not immediately extract more hooks. First ask whether it owns too many responsibilities. Hooks make behavior reusable, but good component boundaries make behavior understandable.

All writing