May 16, 2022

Articles - Tutorials

React v18 New Features Overview And Examples

One of The main purposes of The React team has always been improving the performance of react applications and providing more features and Apis to help the developers achieve that goal.

React v18, the most recent major react update, finally became available as a stable version about a month ago. this new version makes some out of the box improvements and offers new features for improving the performance of react web applications, most importantly the long awaited concurrent features. and it prepares the basis for the future updates.

I’ll do an overview of the new features of the latest stable version of React in this post with examples, which makes this post also a quick tutorial 😉.

Post related codes: CodeSandbox

The Root API and upgrading to React 18

Updating to v18 doesn’t break your existing code, using the root api, react gives us a gateway to the new features like concurrent rendering and we actually just opt-in.

Before getting started we have to update to react ^18 if you have an existing project and still haven’t updated the react version:

npm install react@18 react-dom@18

In order to use the new features of React 18 we have to use the new root api and ReactDOM.createRoot instead of ReactDOM.render to create a root in index.js(or index.tsx if you use typescript) like so:

// index.js
import React from 'react';
import ReactDOM from 'react-dom/clinet';
import App from './App';


const root = ReactDOM.createRoot(document.getElementById("root"));

root.render(<App />);

we have to import ReactDom from react-dom/clinet

Note that if you use Typescript, you have to update the @types/react-dom as well and handle the possibility of rootElement being null otherwise you’ll face TS type errors

This is how I got react v18 working with my existing react Typescript project:

npm i @types/react-dom@^18.0.0
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const rootElement = document.getElementById("root")
if (!rootElement) throw new Error('root element not found!');
const root = ReactDOM.createRoot(rootElement);

root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Now that we have updated our project and we are up and running with react 18(or created a new project with react v18^) lets learn about the new features.

1- Automatic Batching for All State Updates

The first feature of react 18 is an out of the box improvement, Automatic Batching means React will combine all state updates at a time into one update, and this will cause only one re-render, react already did auto batching in the state updates inside event handlers but since version 18 all state updates will be auto batched.

for example the below state updates will be batched into one update and cause one rerender:

function handleVerify(){
  setStatus("Verified");
  setStep(2)
}

2- Concurrency: startTransition, useTransition and useDeferredValue

What is concurrency in React?

If an expensive calculation and heavy re-render is based on a state, it might make other elements and components that react to that state updates slow, for example if you want to search in a big list of items based on the value of an input which updates a state or a heavy calculation is made when the input state changes (like a heavy recursive function, etc), the input might have delays or lag when the user types and therefor have a very negetive impact on user experience.

React’s new solution for this problem was prioritizing state updates, so concurrency features are features and Api’s to help us prioritize urgent and interactive updates over updates that take a longer time. and with these features we can specify less urgent values.

These features include the new useDeferredValue and useTransition hooks and the startTransition method.

useTransition and startTransition

With this hook we can specify some state updates to be handled with lower priority than normal state updates.

This hook provides an isPending value and the startTransition function, isPending obviously is a boolean that is true if the specified update is waiting for other state updates to complete, with startTransition we can specify updates as Transitions which have less priority and stay pending until normal updates finish.

In the below example, hypothetically re-rendering the List component is so expensive that it might cause lag for the input, the list has 10000 items and it’s filtered whenever the user types in the search input and both the input and the list are updated at the same time, but by using startTransition the list update will wait for input to re-render and the input will remain smooth even if the operation made in the List component is expensive and takes time, without startTransition the input lags heavily but with startTransition it doesn’t:

// UseTransitionExample.js

import React, { useState, useTransition } from "react";
import List from "./components/List";
import { generateList } from "./data";

const list = generateList();

function filterList(query) {
  return list.filter((item) => item.title.includes(query));
}

function UseTransitionExample() {
  const [query, setQuery] = useState("");
  //
  const [isPending, startTransition] = useTransition();

  const filteredList = filterList(query);

  function handleSearchChange(e) {
    startTransition(() => setQuery(e.target.value));
  }

  return (
    <>
      <input id="search" type="search" onChange={handleSearchChange} />
      {isPending ? <div>Searching...</div> : <List list={filteredList} />}
    </>
  );
}

export default UseTransitionExample;

Of course we never query 10000 items in the client and such operations are always handled in the backend but this example shows that how much useTransition can be helpful. instead of filter there could be any expensive operation. we can also use chrome dev tools to throttle the CPU to emulate weaker devices like phones.

Here when I type in the input without useTransition we can see that the input has a lot of delay and even kinda seems stuck, but by using concurrency the input is smooth and updating the list is delayed until the input finishes updating and I show a fallback “searching…” text when the list update is pending using isPending.

We can also import and use startTransition without useTransition hook in class components and where hooks are unavailable.

useDeferredValue

useDeferredValue is another hook that enables us to use React’s concurrency features, it’s difference with useTransition is that it can be used when the value comes from above like a third party library and we don’t have any control on it’s corresponding setState function.

useDeferredValue receives a value and returns a new value that will defer to more important updates. If the current render is the result of an important update, React will return the previous value and then render the new value after the important render has completed.

We could do the same thing we did with useTransition with useDeferredValue, for example we imagine that in the DeferrdList component we don’t have any control on the list prop and it’s from a third party library or a value that we don’t have access to it’s setState, in this situation we can use useDefetredValue:

// DeferrdList.js

import { useDeferredValue } from "react";

function List({ list }) {
  const deferredList = useDeferredValue(list);

  const listItems = deferredList.map((item) => (
    <li key={item.id}>{item.title}</li>
  ));
  return <ul className="list">{listItems}</ul>;
}

export default List;
// UseDeferredValueExample.js

import React, { useState } from "react";
import DeferredList from "./components/DeferredList";
import { generateList } from "./data";

const list = generateList();

function filterList(query) {
  return list.filter((item) => item.title.includes(query));
}

function UseDeferredValueExample() {
  const [query, setQuery] = useState("");
  //

  const filteredList = filterList(query);

  function handleSearchChange(e) {
    setQuery(e.target.value);
  }

  return (
    <>
      <input
        id="search"
        type="search"
        onChange={handleSearchChange}
        placeholder="Search..."
      />
      <DeferredList list={filteredList} />
    </>
  );
}

export default UseDeferredValueExample;

Another advantage of useDeferredValue is that It can be used with useMemo to rerender a child component only when a deferred value changes and not when the input changes and we can show a fallback when it all happens with Suspense like so:

function DeferredExample() {
  const search = useState('');
  const deferredSearch = useDeferredValue(search);


  const suggestions = useMemo(() =>
    <SearchSuggestions search={deferredSearch} />,
    [deferredSearch]
  );

  return (
    <>
      <SearchInput search={search} />
      <Suspense fallback="Searching...">
        {suggestions}
      </Suspense>
    </>
  );
}

While the concurrency features are awesome you shouldn’t use them for every value or in every component just like useCallback, useMemo and Memo these features should be used only as an optimization technique.

3- useId hook

The new useId hook returns a random and unique string and it works in both client and server and every time the id would be different, the main usage of useId is creating unique id’s for linked html elements like label and input and the generated id’s with useId can’t be referred to with css since they change on every reload:

// UseIdExample.js

import React, { useId } from "react";

function UseIdExample() {
  const nameId = useId();

  return (
    <form>
      <label htmlFor={nameId}>Name:</label>
      <input id={nameId} name="Name" />
    </form>
  );
}

export default UseIdExample;

You might think that useId is good for using it as the key prop for components but it’s not, because key should be related to items so that react can keep track of them like item ids.

4- Suspense component improvements and new capabilities

Suspense could already be used for waiting for lazy loaded components but with React 18 it has been improved and it will become even more useful in the future updates. the most important improvement to suspense is that unlike previous versions, it can now be used with server side rendering. one of the upcoming features for Suspense is being able to use it for data fetching and showing a fallback when the api request hasn’t finished, which is currently usually done manually.


React 18 added multiple important features and improvement and there are also other exciting upcoming features which v18 sets the foundation for them. hopefully all these new features will help us to build better and more performant applications!