DevPath · Learn to code ESPTEN

Modern React: Suspense, lazy, Portals and concurrency

Advanced refs and concurrency

forwardRef: exposing a ref through a component

refs aren't passed like a normal prop. If you want a parent component to get a reference to a node inside a child component, that child must forward the ref with React.forwardRef:

const Input = React.forwardRef(function Input(props, ref) {
  return <input ref={ref} {...props} />;
});

function Form() {
  const inputRef = React.useRef(null);
  // inputRef.current will point to the child's real <input>
  return <Input ref={inputRef} />;
}

useImperativeHandle: customizing what the ref exposes

Sometimes you don't want to give access to the entire DOM node, but only to a few specific actions (a small imperative API). useImperativeHandle defines exactly what the parent will see through the ref:

const Field = React.forwardRef(function Field(props, ref) {
  const inputRef = React.useRef(null);
  React.useImperativeHandle(ref, () => ({
    focus: () => inputRef.current.focus(),
    clear: () => { inputRef.current.value = ""; },
  }));
  return <input ref={inputRef} />;
});

// The parent can call fieldRef.current.focus() — and nothing else.

It's an exceptional pattern: use it only for real imperative actions (focus, scroll, play/pause). For data, keep preferring props and state.

Concurrent hooks: keeping the UI responsive

Modern React can interrupt and prioritize renders. Two hooks take advantage of this so that expensive work doesn't freeze the interface:

useTransition

Marks a state update as non-urgent (a transition). React processes it with low priority, letting urgent interactions (typing in an input) respond instantly. It also gives you an isPending to show a loading indicator:

const [isPending, startTransition] = useTransition();

function search(text) {
  setText(text);                   // urgent: the input updates now
  startTransition(() => {
    setResults(filter(text));      // non-urgent: can wait/be interrupted
  });
}

useDeferredValue

Takes a value and returns a "deferred" version of it. While a new value arrives, React can keep showing the previous one, avoiding recalculating a heavy list on every keystroke:

const deferredQuery = useDeferredValue(query);
// The expensive list is filtered with deferredQuery, not with query.

Both pursue the same goal: making typing or clicking feel fluid even though there's an expensive render in the background.

useId: unique and stable identifiers

React.useId generates a unique, stable id for the component, identical on the server and the client (key to avoiding hydration errors). Its star use is accessibility: linking a <label> with its <input>.

function Field() {
  const id = React.useId();
  return (
    <div>
      <label htmlFor={id}>Name</label>
      <input id={id} />
    </div>
  );
}

This way you don't need to invent ids by hand (which would collide if the component is used several times on the page). Never use useId to generate list keys: it's for DOM element ids.

In the code exercise you'll practice React.useId (synchronous and validatable). The concurrent hooks useTransition and useDeferredValue are evaluated in the quizzes, since their effect depends on React's asynchronous scheduler.

Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Portals: rendering outside the DOM treeView the module →