Course outline

React / Coordinated application behavior / Lesson 8

Refs and imperative boundaries

Use refs for values that do not drive rendering and for narrow DOM operations with explicit lifecycle ownership.

About 4 min readContent reviewed
On this page

Lesson goals

  • A ref is persistent storage without rendering
  • Focus in response to a deliberate action
  • Design the public imperative surface narrowly

A ref is persistent storage without rendering

useRef returns a stable object whose current property can hold a value across renders. Changing current does not request a render, so a visible counter should normally use state instead. Refs suit timer handles, DOM nodes, and other mutable values that should not determine the returned UI directly. Avoid reading or writing refs during render except documented predictable initialization patterns, because rendering must remain a pure calculation. A ref is not a general escape hatch from dependency rules. If the interface needs to react to a value, model that value as state or through an appropriate external-store integration.

Keep React’s DOM ownership intact

A DOM ref exposes the committed node so an event handler can focus, scroll, or interact with a browser API. The node may be null before mounting or after removal. Do not manually rewrite children that React manages, since the renderer’s model and actual DOM can diverge. Prefer a state-driven description when changing content or classes. A narrow focus call is different from imperatively building a second interface inside a managed subtree. When integrating a third-party widget, define which container and descendants it owns, how updates are passed, and how cleanup releases it.

Focus in response to a deliberate action

The first example lets a native button focus a labeled input. The handler checks the current node and performs only that narrow operation. It does not change the field’s value imperatively or hide focus styling. Moving focus should support the task, not occur on every render or whenever an unrelated state changes. Test keyboard and pointer activation and confirm that focus is not sent into a hidden region. If a dialog or error summary needs focus management, document initial placement, dismissal, and restoration as a complete interaction pattern instead of scattering focus calls throughout components.

jsx Component module — requires the course React host
import { useRef } from "react";
export default function App() { const input = useRef(null); return <main><h1>Focus helper</h1><label htmlFor="search">Search</label><input id="search" ref={input} /><button type="button" onClick={() => input.current?.focus()}>Focus search</button></main>; }
Expected output
Clicking Focus search makes the Search input active.

The event performs a narrow DOM operation on the committed input. The native label and input retain their semantics.

Store an operation handle, not its display state

The second example stores a timeout handle in a ref and the visible status in state. Starting again clears the previous timeout, and cleanup clears any remaining handle on unmount. The timeout uses fixed local synthetic behavior rather than a service call. The handle itself does not need to render, while “Waiting” and “Done” do. This division prevents a state update merely to remember an implementation resource. A timeout is not precise scheduling; browsers may delay callbacks, so the contract is eventual completion while mounted, not an exact millisecond display guarantee. Tests should control or await the owned condition.

jsx Component module — requires the course React host
import { useEffect, useRef, useState } from "react";
export default function App() { const timer = useRef(null); const [status, setStatus] = useState("Idle"); useEffect(() => () => clearTimeout(timer.current), []); function start() { clearTimeout(timer.current); setStatus("Waiting"); timer.current = setTimeout(() => setStatus("Done"), 20); } return <main><h1>Owned timer</h1><p>{status}</p><button type="button" onClick={start}>Start</button></main>; }
Expected output
Initial: Idle. After Start: Waiting, then Done while mounted.

The ref stores the handle and state stores the visible result. Starting again replaces the owned timeout instead of accumulating callbacks.

Design the public imperative surface narrowly

A parent sometimes needs an imperative operation such as focus, but exposing an entire DOM subtree can couple it to private implementation. Prefer ordinary props for declarative behavior and callbacks for events. Where an imperative handle is justified, expose the smallest documented operation set and verify lifecycle behavior. Ref-passing APIs differ across React versions: React 19 supports ref as a prop for function components, while older patterns use forwardRef. This course’s examples use local refs shared across the tested baseline and current docs rather than silently mixing those version-specific component APIs. Never use a ref change as proof that a render has committed without coordinating the actual lifecycle.

Common errors and fixes

Changing ref.current does not update a label

Why it happens
Refs do not request rendering.
How to fix it
Use state for displayed information.
Prevent it next time
Separate resource handles from UI data.

A DOM call fails before mounting

Why it happens
The ref node is still null.
How to fix it
Invoke at a valid lifecycle boundary and guard absence.
Prevent it next time
Test mount and unmount paths.

Manual DOM edits disappear or conflict

Why it happens
They changed children owned by React.
How to fix it
Describe managed content through state and props.
Prevent it next time
Document ownership around external widgets.

Check your understanding

1. Choose state or a ref for a selected tab and a pending timeout handle.

Show worked solution

Use state for the selected tab because it determines visible content and accessibility state. Use a ref for the timeout handle because it identifies an implementation resource that must be cleared but need not render. Keep pending status separately if users need to see it.

2. Why should an external widget integration define a container owner?

Show worked solution

Both React and the widget can otherwise manipulate the same descendants, making either system’s model stale. Give the widget a deliberate boundary, synchronize its documented inputs, and dispose it during cleanup. Keep React-owned semantic content outside any uncontrolled mutation region.

Key takeaways

  • Refs persist without scheduling renders.
  • Imperative DOM access needs a narrow contract.
  • Resource cleanup remains necessary.

Official references