React v18.0 has broken ground by introducing a long-awaited feature: Concurrency!

Unfortunately, despite a deluge of resources explaining how to use it, explanations of how it works are sparse.

As it is a low-level feature, it’s not critical to understand React’s idea of concurrency, but it doesn’t hurt!

This post does not attempt to exhaustively document React’s Concurrent API and best practices. It is best read alongside React’s documentation pages which are linked throughout.

What is React Concurrency?

The basic premise of React concurrency is to re-work the rendering process such that while rendering the next view, the current view is kept responsive.

Concurrent Mode was a proposal the React team had to improve application performance. The idea was to break up the rendering process into interruptible units of work.

Under the hood, this would be implemented by wrapping component renders in a requestIdleCallback() call, keeping applications responsive during the rendering process.

So hypothetically, if Blocking Mode were implemented like this:

function renderBlocking(Component) {
    for (let Child of Component) {
        renderBlocking(Child);
    }
}

Then Concurrent Mode would be implemented like this:

function renderConcurrent(Component) {
    // Interrupt rendering process if state out-dated
    if (isCancelled) return;

    for (let Child of Component) {
        // Wait until browser isn't busy (no inputs to process)
        requestIdleCallback(() => renderConcurrent(Child));
    }
}

Read the Practical Guide To Not Blocking The Event Loop to understand why this keeps the application responsive!

If you’re curious how React does this in reality, take a peek at the implementation of React’s scheduler package. After initially using requestIdleCallback, React switched to requestAnimationFrame, and later to a user-space timer.

No Mode, Only Features

The Concurrent Mode plan did not materialize for backward-compatibility reasons.

Instead, the React team pivoted to Concurrent Features, a set of new APIs selectively enabling concurrent rendering. So far, React has introduced two new hooks to opt into a concurrent render.

useTransition

The useTransition hook returns two items:

  1. Boolean flag isPending, which is true if a concurrent render is in progress
  2. Function startTransition, which dispatches a new concurrent render

To use it, wrap setState calls in a startTransition callback.

function MyCounter() {
    const [isPending, startTransition] = useTransition();
    const [count, setCount] = useState(0);
    const increment = useCallback(() => {
        startTransition(() => {
            // Run this update concurrently
            setCount(count => count + 1);
        });
    }, []);
 
    return (
        <>
            <button onClick={increment}>Count {count}</button>
            <span>{isPending ? "Pending" : "Not Pending"}</span>
            // Component which benefits from concurrency
            <ManySlowComponents count={count} />
        </>
    )
}

Try it out:

Conceptually, state updates detect if they are wrapped in a startTransition to decide whether to schedule a blocking or concurrent render.

function startTransition(stateUpdates) {
  isInsideTransition = true;
  stateUpdates();
  isInsideTransition = false;
}

function setState() {
  if (isInsideTransition) {
    // Schedule concurrent render
  } else {
    // Schedule blocking render
  }
}

An important caveat of useTransition is that it cannot be used for controlled inputs. For those cases, it is best to use useDeferredValue.

useDeferredValue

The useDeferredValue hook is a convenient hook for cases where you do not have the opportunity to wrap the state update in startTransition but still wish to run the update concurrently.

An example of where this occurs is child components receiving a new value from the parent.

Conceptually, useDeferredValue is a debounce effect and can be implemented as such:

function useDeferredValue<T>(value: T) {
    const [isPending, startTransition] = useTransition();
    const [state, setState] = useState(value);
    useEffect(() => {
        // Dispatch concurrent render
        // when input changes
        startTransition(() => {
            setState(value);
        });
    }, [value])

    return state;
}

It is used the same way as an input debounce hook:

function Child({ value }) {
    const deferredValue = useDeferredValue(value);
    // ...
}

Concurrent Features & Suspense

The useTransition and useDeferredValue hooks have a purpose beyond opting in concurrent rendering: they also wait for Suspense components to complete.

A future post will cover the topic of Suspense and their roles in it. Subscribe to the newsletter to be the first to read it.