React 18 focuses more on the user experience and performance. There are quite a few features that are introduced in this version like Automatic Batching, Concurrent features, New Hooks, New Suspense features, Strict mode behaviours.

For this article lets concentrate on two new hooks(useTransition, useDeferredValue) that are introduced and most importantly when to use them?

These hooks are based on two concepts introduced in React18. One is Concurrency and the other Transitions. Lets talk about these concepts first.

Concurrency

The most important property of Concurrent React is that rendering is interruptible. Pre React 18 we see that once the update starts rendering, nothing can interrupt it until the user can see the result on the screen. But in concurrent render, React may start rendering an update, pause in the middle, then continue later.

Which means that tasks can overlap, rather than wait for one state update to completely finish before moving on to the next one, concurrency allows us to bounce back and forth between multiple tasks. It does not mean that everything is happening at the same time rather one task can be paused to attend to an urgent task and once this is completed we can jump back to less urgent task and bring back the updated information.

For Example if we have application layout as seen below:

A simple application layout which shows user info, posts, comments and navbar to navigate between tabs.

Lets say that Navbar and Posts are loaded but UserInfo & Comments are not. Pre-React 18 the application will not become interactive unless everything is loaded. So it waits until UserInfo & Comments to load before becoming interactive. But using React 18 and the above explained concurrency feature will make the sections that are already loaded to be interactive and great benefit of it is that users are no longer stuck with the page that they cant navigate away from.

A simple application layout which shows user info, posts, comments and navbar to navigate between tabs. Here user info is in the process of loading. while navbar & posts are already loaded. comments section is yet to be loaded.

And lets consider the above example now where UserInfo is loading and Comments are yet to be loaded. And if user clicks on Comments section now react will pause loading of UserInfo and starts loading the Comments. Once this is completely loaded then react will go back and continue to load the UserInfo section. So we can see that React prioritises the part of application the user finds most interesting. This make the user experience much better.

Transitions

This term is used for animations like css transitions. That seems to be the reason behind naming it this way as tweeted by Dan.

The basic definition of transition is that it informs React to de-prioritise or move the less urgent action or tasks to the back burner.

Setting up the project

For this article, I have created a small application for searching cryptocurrency using code sandbox. The app is very simple it has a input box where user can search for a cryptocurrency and below the search bar the results are displayed as a grid with two columns

  1. Cryptocurrency Name
  2. Percentage change

Lets look at the components We have a SearchBar:

const SearchBar = ({ name, placeHolder, handleChange }) => {
  return (
    <div className="searchbar">
      <input
        title={name}
        placeholder={placeHolder}
        name={name}
        type="text"
        onChange={handleChange}
      />
    </div>
  );
};

A Grid component

const Grid = ({ data }) => (
  <div className="grid-container">
    {data
      ? data.map((item, index) => {
          return (
            <div key={index} className="row">
              <div className="col">{item.name}</div>
              <div
                className={`col ${
                  item.percent_change < 0 ? "negative" : "positive"
                }`}
              >
                {item.percent_change !== 0
                  ? (item.percent_change * 100).toFixed(2) + "%"
                  : null}
              </div>
            </div>
          );
        })
      : null}
  </div>
);

And an App component which gets the results from Coinbase open search api which returns the matching cryptocurrencies for the entered text in SearchBar.

export default function App() {
  const [query, setQuery] = useState("");
  const [assets, setAssets] = useState([]);

  const makeApiCall = (value) => {
    let URL = value
      ? `https://www.coinbase.com/api/v2/assets/search?query=${value}`
      : "https://www.coinbase.com/api/v2/assets/search";
    fetch(URL)
      .then((response) => response.json())
      .then((data) => {
        setAssets(
          new Array(1000)
            .fill([])
            .map((item) => data.data)
            .reduce((curr, prev) => [...curr, ...prev], [])
        );
      });
  };

  const handleChange = (e) => {
    const { value } = e.target;
    setQuery(value);
    makeApiCall(value);
  };

  return (
    <div className="App">
      <h1>CryptoLive</h1>
      <SearchBar
        name={"search"}
        placeHolder={"Start by typing Bitcoin.."}
        handleChange={handleChange}
        isLoading={isLoading}
        value={query}
      />
      {assets.length > 0 && <h5>{assets.length} records</h5>}
      <Grid data={assets} />
    </div>
  );
}

You can see in the above fetch api call, I have duplicated the results to form a huge data array so that we can clearly see how the application behaves with huge data(> 20,000 records). I have already implemented it here

A preview of the demo application can be seen in the below image:

CryptoLive Application - Shows the live percentage change of cryptocurrencies

Try to play with the application by entering your favourite cryptocurrency in the search bar. You will notice that the SearchBar gets frozen when the results are fetching and it won’t allow you to type until the results are fetched and rendered on the UI.

This is a bad experience for the user, fetching of the results should not block the user from typing into the SearchBar. It would discourage the user to use the application. And also imagine users who access the application with slow network or slow CPU, it would be a frustrating experience for them.

Pre-React 18 the standard solution will not work on more than 10,000 items at a time. We may leverage techniques like Pagination, Virtualisation, Skeleton UI to make loads feel faster, Debouncing, SetTimeouts or server side rendering instead of client side as it would make the page unresponsive or clunky.

As mentioned in the beginning of the article the concept of Concurrency, Transitions and the new hooks will help to solve this issue.

Implementing the useTransition()

useTransition allows us to mark the less urgent task as transitions which tells react to let other more urgent actions to take priority in the rendering timeline. This means that transitions can be interrupted by more pressing updates.

When we call useTransition, we get an array with two elements:

isPending: Boolean value that Indicated if the low-priority state update is still pending.

startTransition(): Function that you can wrap around a state update to tell React not to worry and concentrate on more urgent tasks and come back when it is not busy and execute the less important tasks.

Lets see how we can update our project to use useTransition.

export default function App() {
  const [query, setQuery] = useState("");
  const [assets, setAssets] = useState([]);
  const [isPending, startTransition] = useTransition()

  const makeApiCall = (value) => {
    let URL = value
      ? `https://www.coinbase.com/api/v2/assets/search?query=${value}&limit=20`
      : "https://www.coinbase.com/api/v2/assets/search?limit=20";
    fetch(URL)
      .then((response) => response.json())
      .then((data) => {
        startTransition(() => {
          setAssets(
            new Array(1000)
              .fill([])
              .map((item) => data.data)
              .reduce((curr, prev) => [...curr, ...prev], [])
          );
        });
      });
  };

  const handleChange = (e) => {
    const { value } = e.target;
    setQuery(value);
    makeApiCall(value);
  };

  return (
    <div className="App">
      <h1>CryptoLive (with useTransition)</h1>
      <SearchBar
        name={"search"}
        placeHolder={"Start by typing Bitcoin.."}
        handleChange={handleChange}
        isLoading={isLoading}
        value={query}
      />
      {isPending && <div> Loading....</div>}
      {assets.length > 0 && <h5>{assets.length} records</h5>}
      <Grid data={assets} />
    </div>
  );
}

In the above code the most time taking and low priority state update is setAssets so I am wrapping it inside startTransition, Now React gives this state update a low priority. This makes the SearchBar responsive and responds to keystrokes instantly in the application. Now if you type in the currency name, you will see the code more responsive and less laggy, even if the CPU has been slowed down 4 or 6 times. Try it out.

You can see the difference in responsiveness of the application here after adding useTransition hook.

Implementing useDeferredValue()

useDeferredValue does the same thing as useTransition but in a slightly different way. It is mainly useful when the value comes ‘from above’ and you don’t actually have control over the corresponding setState call. useTransition helps us by wrapping the state updating code inside which is treated as low priority. However in some cases we may not have access to this state updating code. For Example if third party library handles the state update. This is when useDeferredValue can be used.

Here we don’t wrap the state updating code, instead we wrap the value generated or updated as a result of the state update inside the hook.

In our project we can apply this concept in Grid component because the grid component receives the data which is the result of state update from parent. This is the best place to use our useDeferredValue.

const Grid = ({ data }) => {
  const deferredData = useDeferredValue(data);
  return (
    <div className="grid-container">
      {deferredData
        ? deferredData.map((item, index) => {
            return (
              <div key={index} className="row">
                <div className="col">{item.name}</div>
                <div
                  className={`col ${
                    item.percent_change < 0 ? "negative" : "positive"
                  }`}
                >
                  {item.percent_change !== 0
                    ? (item.percent_change * 100).toFixed(2) + "%"
                    : null}
                </div>
              </div>
            );
          })
        : null}
    </div>
  );
};

Here we wrap data inside useDeferredValue. The way this is going to work is similar to debounce or throttling. But unlike debounce or throttling we don’t specify the time to wait until an action is performed in the case of useDeferredValue. And also useDeferredValue kicks in only on slow computer but debouncing/throttling is done on every computer and every time.

When we wrap the value using this hook, React knows that this value is going to be deferred and it wont do anything until the application is not busy with anything else. And when it is done completing all other tasks it will process the deferred value.

You can see the result of above implementation here

Conclusion

In this article, we went through the most important concepts in React 18 which is Concurrency, Transitions and about the two hooks that are based of these concepts.

Both hooks are similar in function, useTransition wraps the state updating code, whereas useDeferredValue wraps a value affected by the state change.

These hooks should not be used to wrap up all of your state updates or values. If you have a complex user interface or component that can’t be optimised any other way then you should use hooks. Overall both of these hooks help improve the user experience.

Thanks for reading. Hope this helps.