React 18

11. August, 2022 5 min read Develop

Welcome to React 18

React 18 introduces features powered by their new concurrent renderer. Version 18 provides gradual adoption strategies for existing applications with a simple upgrade path.

Their new concurrency system is a new behind-the-scenes mechanism that enables React to prepare multiple versions of the UI simultaneously, adding more speed and efficiency. Naturally, we want to profit from that and upgrade our existing applications to React 18.

How to Upgrade to React 18

The first warning you will see after upgrading to React 18 is that ReactDOM.render is no longer supported. You have to upgrade to ReactDOM.createRoot;

// Before React 18
import ReactDOM from 'react-dom';
import App from './App';

const root = document.getElementById('root');
ReactDOM.render(<App />, root);

// After React 18
import ReactDOM from 'react-dom';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

The same applies to ReactDOM.hydrate, which we covered in the previous article.

There are some smaller changes to unmountComponentAtNode as well:

// Before React 18
unmountComponentAtNode(container);

// After React 18
root.unmount();

You can find more information in the official blog post.

Automatic Batching

In React, we are used to updating states as follows:

setLoading(true);
setData(undefined);

React is smart and tries to batch those state changes automatically together to avoid unnecessary re-renders. However, the challenge is when React should do this. This automation includes event handlers but no promises, setTimeouts and event callbacks.

fetch('/api/').then(({ data }) => {
  setLoader(true); // re-render
  setData(data);   // re-render
})

React 18 introduces automatic batching for promises, setTimeouts and event callbacks. You can opt out by using flushSync:

flushSync(() => setLoader(true));

Transitions

Sometimes you want certain updates to be treated more urgent than others. For example, clicking or pressing are immediate responses that require immediate feedback. You can still load the content after a click, but the UI should indicate this immediately. So at best, we trigger an urgent update and transition to a non-urgent one:

import { startTransition } from 'react';

// show the input change immediately
setInputValue(input);

// fetch the data in a non-urgent manner
startTransition(() => setSearchQuery(input));

Additionally, useTransition is available as a hook to track the pending state.

Suspense

Wouldn’t it be nice to make the “UI loading state” a first-class declarative concept in the React programming model? This issue is what the <Suspense /> component solves in React 18. It shows the respective loading component if the corresponding component tree is not yet ready to be displayed:

<Suspense fallback={<AvatarLoading />}>
  <Avatar />
</Suspense>

Earlier versions of React had a limited understanding of Suspense. However, the only supported use case was code splitting using React.lazy, which wasn’t supported when rendering on the server.

React 18 added support for Suspense on the server while expanding its capabilities using concurrent rendering features.

New Client and Server Rendering APIs

Most of the features have been mentioned in the previous article. They include changes to React Hydration and how to stream data efficiently between SSR and CSR.

Have a closer look at React 18’s documentation if you want to dive further into the details and examples.

New Strict Mode Behaviors

In the future, React will add a feature that allows adding and removing sections in the UI while preserving the state. For example, when a user tabs away from a screen and back, future React will be able to show the previous screen immediately.

This approach requires React to be resilient to effects being mounted and destroyed multiple times. To help surface these issues, React 18 introduced a new development-only check to Strict Mode. This new check will automatically unmount and remount every component whenever a component mounts for the first time, restoring the previous state on the second mount and looks like this:

* React mounts the component.
  * Layout effects are created.
  * Effects are created.
* React simulates unmounting the component.
  * Layout effects are destroyed.
  * Effects are destroyed.
* React simulates mounting the component with the previous state.
  * Layout effects are created.
  * Effects are created.

See further information about ensuring a reusable state.

New Hooks

Besides useTransition there have been new hooks introduced to make life easier:

useId

It is not intended for generating keys in a list. Keys need to be generated from your data.

const id = useId();
const CustomForm = () => (
  <>
    <label htmlFor={id}>Accept</label>
    <input id={id} type="checkbox" name="react" />
  </>
);

useDeferredValue

It accepts a value and returns a new copy of the value that will defer to more urgent updates.

const query = useSearchQuery('');
const deferredQuery = useDeferredValue(query);

// we wait for the deferredQuery to be ready
// to avoid unnecessary re-renders
const suggestions = useMemo(() =>
  <SearchSuggestions query={deferredQuery} />,
  [deferredQuery]
);

// but we still want the query input to be immediate
return (
  <>
    <SearchInput query={query} />
    <Suspense fallback="Loading results...">
      {suggestions}
    </Suspense>
  </>
);

useSyncExternalStore

It is a new hook that allows external stores to support concurrent reads by forcing updates to the store to be synchronous. It removes the need for useEffect when implementing subscriptions to external data sources and is recommended for any library that integrates with state external to React.

// general use case
const state = useSyncExternalStore(
  store.subscribe, store.getSnapshot
);

// or subscribe to a specific field
const selectedField = useSyncExternalStore(
  store.subscribe,
  () => store.getSnapshot().selectedField,
);

useInsertionEffect

This new hook allows CSS-in-JS libraries to address performance issues of injecting styles in the render. You probably won’t use this unless you build your CSS-in-JS library, so I refer to the documentation instead.

Conclusion

React 18 introduced little changes that set the foundation for future releases. At the same time, it keeps a straightforward upgrading path for us developers. Check out their keynote to learn more about React 18 and its future.

‘Till next time!