Apr 26, 2022

New startTransition API in React

React new startTransition API

What is a Transition?

A transition is a new concept in React to distinguish between urgent and non-urgent updates.

  • Urgent updates reflect direct interaction, like typing, clicking, pressing, and so on.
  • Transition updates transition the UI from one view to another.

Urgent updates like typing, clicking, key pressing need immediate response to match our intuitions about how physical objects behave. Otherwise they feel "wrong". However, transitions are different because the user doesn’t expect to see every intermediate value on screen.

For example, when you type in a search box you expect the input field itself to respond and show the typed character immediately when you press a key. However, the actual results may transition separately and a small delay would be often expected. If you change the search term again before the results are done rendering, you only care to see the latest results.

In React 18 we can use the startTransition API to mark any state updates as non-urgent.

In the code block below, there are two state updates - one that is urgent and one that is marked as non-urgent as it's wrapped inside the startTransition callback:

import {startTransition} from 'react';

// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions (non-urgent)
startTransition(() => {
  // Transition: Show the results
  setResults(input);
});

Non-urgent updates will be interrupted if more urgent updates like clicks or key presses come in. If a transition gets interrupted by the user (for example, by typing multiple characters in a row), React will throw out the stale rendering work that wasn’t finished and render only the latest update.

Demo Application: Performance with and without startTransition

We've built a simple application to demonstrate the power of the new startTransition API.

The application is basically a "Text Search" feature that provides the user with the ability to search a term inside a long block of text (in our case about 60000 characters).

Our application needs to provide two features:

  • To render an input that allows the user to type a search term.
  • To highlight any characters that match (case insensitive) with the typed search term.

The naive algorithm we used to get the matched terms is:

  1. Construct a regex from the search term that will match any characters (case insensitive).
  2. Add the highlight class to all the spans of the matched characters.

The code for the applyFilter function is seen below:

const applyFilter = (query, setFilteredNode) => {
  setFilteredNode(() => {
    if (!query || query.trim().length === 0) {
      return INITIAL_VALUE;
    }

    const regex = new RegExp(
      query.trim().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"),
      "ig"
    );

    const node = TEXT.replaceAll(regex, (s) => `##${s}##`)
      .split("##")
      .map((s, i) => (
        <span key={i} className={i % 2 === 1 ? "highlight" : ""}>
          {s}
        </span>
      ));

    return node;
  });
};

Obviously we could come up with more optimized algorithms - and this regex seriously hinders the performance - however our task here is to "create" a costly function that results in a heavy React state update so that we can see if the startTransition API will improve the situation.

You can find all the code of the app here.

1st Implementation: Unoptimized without startTransition

Our first implementation is a component with two simple state updates, one for the input value and one that will apply the filtered results. Whenever the input value changes, the function applyFilter will be called. Both state updates are marked as urgent by default:

import {useState} from 'react';

const Unoptimized = () => {
  const [query, setQuery] = useState("");
  const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);

  const onInputChange = (e) => setQuery(e.target.value);

  useEffect(() => {
    applyFilter(query, setFilteredNode);
  }, [query]);

  return (
    <>
      <div className="input">
        <input onChange={onInputChange} />
      </div>
      <div className="text">{filteredNode}</div>
    </>
  );
};

Unoptimized

You can see that the characters are not displaying immediately in the input field and the UI feels a bit "slow":

Unoptimized

Unoptimized with CPU 4x slowdown

We applied a 4x CPU slowdown (from Chrome dev tools) to better emulate an average CPU or a mobile device. We can see that the UI definitely feels slow and not smooth at all:

Chrome Dev Tools

Unoptimized CPU 4x Slowdown

2nd Implementation: Optimized with startTransition

In our second implementation we utilize the startTransition API. Specifically we make use of the useTransition hook which returns a pending state as well - we use that state to indicate to the user that a transition is still being processed by blurring the screen (we could also show a loader).

We mark the state update of the results as non-urgent by wrapping it inside a startTransition callback:

import {useState, useTransition} from 'react';

const Optimized = () => {
  const [query, setQuery] = useState("");
  const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);
  const [pending, startTransition] = useTransition();

  const onInputChange = (e) => setQuery(e.target.value);

  useEffect(() => {
    startTransition(() => {
      applyFilter(query, setFilteredNode);
    });
  }, [query, startTransition]);

  return (
    <>
      {pending && <div className="fade" />}
      <div className="input">
        <input onChange={onInputChange} />
      </div>
      <div className="text">{filteredNode}</div>
    </>
  );
};

Optimized

Look how fast the characters are displayed on the input screen; it feels natural, smooth and what the user would expect. We blur the screen while the pending variable is true to indicate to the user that a transition is still in process.

Optimized

Optimized with CPU 4x slowdown

We even applied a CPU 4x slowdown to examine how the UI responds. It looks good, even in that scenario!

Optimized CPU 4x Slowdown

Alternative solutions prior to React 18?

Before React 18 we did not have the startTransition API, so how do we fix it in earlier React versions?

The most common solution to face issues like this and to avoid heavy repetitive state updates was to make use of debounce or throttle techniques.

For example we could utilize this awesome use-debounce library and wrap the heavy state update of the results in a debounced callback like this:

import {useState} from 'react';
import {useDebouncedCallback} from 'use-debounce';

const Debounced = () => {
  const [query, setQuery] = useState("");
  const [filteredNode, setFilteredNode] = useState(INITIAL_VALUE);
   
  const onInputChange = (e) => setQuery(e.target.value);

  const debounced = useDebouncedCallback(
    (query, setFilteredNode) => {
       applyFilter(value, setFilteredNode);
    },
    100 // debounce 100 ms
  );  

  useEffect(() => {
    debounced(query, setFilteredNode);
  }, [query, startTransition]);

  return (
    <>
      {pending && <div className="loader" />}
      <div className="input">
        <input onChange={onInputChange} />
      </div>
      <div className="text">{filteredNode}</div>
    </>
  );
};

But there are still a few problems with this approach.

First, the applyFilter function cannot be called in under 100ms from when the user finishes typing (or whatever amount of time you choose). On the other hand, by using the startTransition API, the heavy processing of the results starts as soon as possible, without having to wait an arbitrary amount of time.

Secondly, even if we used throttle instead of debounce to try to solve the first issue, the work of processing the results is un-interruptable. That means that if new urgent updates come in (like key pressing) while the results are still being processed the UI inevitably would be unresponsive. Whereas with the startTransition API, the processing work will be interrupted when the urgent updates come in, keeping in this way the input field responsive.

The End

I hope you found the article informative.

See you soon! 🙂

Loading Comments...