React -18 New features & Hooks

Pooja Mishra
9 min readJul 24, 2022

Hooks
React 18 was released on March 29, 2022. It introduces 5 new hooks in the release:

  • useTransition
  • useDeferredValue
  • useId
  • useSyncExternalStore
  • useInsertionEffect

Set Up Working Environment in Create React App

We use Create React App as a base to explore these new hooks. The following command creates a React project:

npx create-react-app react-release-18
cd react-release-18

The React versions automatically point to React 18. At the time being, It still uses the legacy root API. If you execute npm start, there will be a warning message:

react-dom.development.js:86 Warning: ReactDOM.render is no longer supported in React 18. Use createRoot instead. Until you switch to the new API, your app will behave as if it's running React 17. Learn more: https://reactjs.org/link/switch-to-createroot

The new root API is invoked by createRoot, which adds all of the improvements of React 18 and enables concurrent features. The following src/index.js is modified to have the new root API:

useTransition

useTransition() is a hook for transition. It returns the transition state and a function to start the transition.

const [isPending, startTransition] = useTransition();

React state updates are classified into two categories:

  • Urgent updates — They reflect direct interaction, such as typing, clicking, pressing, dragging, etc.
  • Transition updates — They transition the UI from one view to another.

Updates in a transition yield to more urgent updates. Here is an example of useTransition , placed in src/App.js:

The above application is composed of two components :

  • button : It is a simple button. The display number is controlled by value . Clicking the button increases value and value2 .
  • SlowUI : The component is defined , which generates 100000+ span elements controlled by value2 . It takes longer time to update so many elements. useTransitionreturns the transition state, isPending, and the function to start the transition, startTransition. When startTransition is called , isPending turns true, and SlowUI is half opaque (light colored) with stale data . When the transition finishes, isPending turns false, and SlowUI becomes full opaque (solid colored) with updated data.

Try to remove startTransition to call setValue2(value2 + 1) directly. You can see that the UI is no longer functional with so many updates happening simultaneously.

The useTransition hook returns isPending and startTransition. If you do not need to show special UI for isPending

import { startTransition } from 'react';

useDeferredValue

useDeferredValue(value) is hook that accepts a value and returns a new copy of the value that will defer to more urgent updates. The previous value is kept until urgent updates have completed. Then, the new value is rendered. This hook is similar to using debouncing or throttling to defer updates.

Here is an example for useDeferredValue, placed in src/App.js:

The above application is composed of three components:

  • button : It is a simple button. The display number is controlled by value at . Clicking the button increases value .
  • div : It displays deferredValue.
  • SlowUI : The component is defined , which generates 50000 fixed-number span elements. Although the component does not have props and visually does not update, it takes a long time to update so many elements.

useDeferredValue can be used in conjunction with startTransition and useTransition.

useId

In a Web application, there are cases that need unique ids, for example:

  • <label for="ID">, where the for attribute must be equal to the id attribute of the related element to bind them together.
  • aria-labelledby, where the aria-labelledby attribute could take multiple ids.

useId() is a hook that generates a unique id:

  • This id is stable across the server and client, which avoids hydration mismatches for server-side rendering.
  • This id is unique for the entire application. In the case of multi-root applications, createRoot/hydrateRoot has an optional prop, identifierPrefix, which can be used to add a prefix to prevent collisions.
  • This id can be appended with prefix and/or suffix to generate multiple unique ids that are used in a component. It seems trivial. But, useId was evolved from useOpaqueIdentifier, which generates an opaque id that cannot be operated upon.

The following is an example of useId, placed in src/App.js:

The above application is composed of three components :

  • Comp1: It is defined , which generates and displays one id, :r0:.
  • Comp2: It is defined , which generates one id, :r1:. From this one id, it derives two unique ids, :r1:-1 (for Label 1 + the input field) and :r1:-2 (for Label 2 + the input field).
  • Comp3: It is defined , which generates and displays one id, :r2:. From his one id, it derives three unique ids, :r1:-a, :r1:-b, and :r1:-c, for the aria-labelledby attribute.

Execute the code by npm start. We see the following UI, along with the generated HTML elements in Chrome DevTools.

useSyncExternalStore

useSyncExternalStore is a hook recommended for reading and subscribing from external data sources (stores).

Here is the signature of the hook:

const state = useSyncExternalStore(subscribe, getSnapshot[, getServerSnapshot]);

This method accepts three arguments:

  • subscribe: It is a function to register a callback that is called whenever the store changes.
  • getSnapshot: It is function that returns the current value of the store.
  • getServerSnapshot: It is function that returns the snapshot used during server rendering. This is an optional parameter.

This method returns the value of the store, state.

We create an example of useSyncExternalStore, which reads the current browser window width and displays it on the screen.

Use the following code to replace the existing src/App.js:

The above application calls useSyncExternalStore:

  • subscribe : It registers a callback for the window resize event listener.
  • getSnapshot: It returns the current browser window width.
  • getServerSnapshot : It is for server rendering, which is not needed here, or simply returns -1.

useInsertionEffect

useEffect(didUpdate) accepts a function that contains imperative, possibly effectful code, which are mutations, subscriptions, timers, logging, and other side effects. By default, effects run after every completed render, but the invocation can be controlled with a second argument of an array.

useLayoutEffect has the same signature as useEffect, but it fires synchronously after all DOM mutations. i.e. it is fired before useEffect. It is used to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.

useInsertionEffect is introduced in React 18. It has the same signature as useEffect, but it fires synchronously before all DOM mutations. i.e. it is fired before useLayoutEffect. It is used to inject styles into the DOM before reading layout.

useInsertionEffect is intended for CSS-in-JS libraries, such as styled-components. Since this hook is limited in scope, this hook does not have access to refs and cannot schedule updates.

The following example, placed in src/App.js, compares useEffect, useLayoutEffect, and useInsertionEffect:

The above application has an App and a Child component . Both of them call useEffect, useLayoutEffect, and useInsertionEffect.

These effects are called in the following order:

  • useInsertionEffect child is called.
  • useInsertionEffect app is called.
  • useLayoutEffect child is called.
  • useLayoutEffect app is called.
  • useEffect child is called.
  • useEffect app is called.

Legacy root API

The legacy root API is the existing API called with the ReactDOM.render method. It will create a root running in legacy mode, which is similar to usage in React version 17. It will gradually enforce the usage of the new root API. The legacy root API will be deprecated in upcoming versions.

Refer to the following code example.

import * as ReactDOM from 'react-dom';
import App from 'App';const container = document.getElementById(‘root’);// Initial render.
ReactDOM.render(<App tab="home" />, container);// During an update, React will access the container element again.
ReactDOM.render(<App tab="profile" />, container);

New root API

The new root API will be invoked with the ReactDOM.createRoot method. To use it, first, we have to create the root through the createRoot method with the root element as an argument. Then, we call the root.render method and pass the app component as the parameter. By using the new root API, we can use all the enhancements and concurrent features available in React 18.

Refer to the following code.

import * as ReactDOM from 'react-dom';
import App from 'App';// Create a root.
const root = ReactDOM.createRoot(document.getElementById('root'));
// Initial render: Render an element to the root.
root.render(<App tab="home" />);// During an update, there's no need to access the container again since we have already defined the root instance.
root.render(<App tab="profile" />);

Changes in hydrate method

The hydrate method is similar to the render method. But it helps to attach event listeners to the HTML elements within the containers that are rendered by the ReactDOMServer method on the server-side.

React 18 replaces this hydrate method with the hydrateRoot method.

Refer to the following code example.

import * as ReactDOM from 'react-dom';import App from 'App';const container = document.getElementById('app');// Create and render a root with hydration.
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// Unlike the createRoot method, you don't need a separate root.render() call here.

Changes in render callback

The render callback is removed from the new root API. But we can pass it as a property to the root component.

Refer to the following code example.

import * as ReactDOM from 'react-dom';function App({ callback }) {
// Callback will be called when the div element is first created.
return (
<div ref={callback}>
<h1>Hello </h1>
</div>
);
}
const rootElement = document.getElementById("root");const root = ReactDOM.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);

Automatic Batching

Batching is multiple React state updates in a single re-render.

function Counter() {
const [count, setCount] = useState(0);
const [disabled, setDisabled] = useState(false); function handleClick() {
setCount(c => c + 1); // does not re-render yet
setDisabled(f => !f); // does not re-render yet
// React will only re-render once (that's batching!)
} return (
<div>
<h1>{count}</h1>
<button disabled={disabled} onClick={handleClick}>
Increment
</button>
</div>
);
}

Above, we have count and disabled two React states. When we click a button, React always batches and updates them in a single re-render.

This avoids unnecessary re-renders and improves our performance. What about asynchronous requests? React 17 does not batch and does two independent updates.

function Counter() {
const [count, setCount] = useState(0);
const [disabled, setDisabled] = useState(false); function handleClick() {
doPostRequest()
.then(() => {
setCount(c => c + 1); // does re-render
setDisabled(f => !f); // does re-render
})
} return (
<div>
<h1>{count}</h1>
<button disabled={disabled} onClick={handleClick}>
Increment
</button>
</div>
);
}

Now, in React 18, we can batch state updates inside promises, setTimeout, native event handlers, or any other event.

Disable automatic batching

Sometimes, we need to immediately re-render the component after each state change. In that scenario, use the flushSync method to disable the automatic batching.

import { flushSync } from 'react-dom'; // Note: react-dom, not reactfunction handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React has updated the DOM by now.
flushSync(() => {
setFlag(f => !f);
});
// React has updated the DOM by now.
}

Server-side rendering

With Suspense, React 18 makes major performance improvements to SSR by making serving parts of an app asynchronously possible.

Server-side rendering allows you to generate HTML texts from the React components served, after which the JavaScript code loads and merges with the HTML (known as hydration).

Now, with Suspense, you can break your app into small, stand-alone units that can be rendered on their own without the rest of the app, allowing content to be available to your user even much faster than before.

Say you have two components: a text component and an image component. If you stack them on each other like this:

<Text />
<Image /&gt;

Then, the server tries to render them at once, slowing the entire page down. If text is more important to your readers, you can give it priority over images by wrapping the Image component in Suspense tags:

<Text />
<Suspense fallback={<Spinner />}>
<Image />
</Suspense>

This time around, the server serves your text component first, and a spinner displays while your image waits to load.

Transitions

One of the most significant updates of React 18 is the introduction of startTransition API that keeps your app responsive even during the large screen updates.
Sometimes during heavy update operations, your app became unresponsive, the startTransition API can be very useful to handle such situations.
The API allows users to control the concurrency aspect to improve user interaction. It is done by wrapping heavy updates as “startTransition” and will be interrupted only if more urgent updates are initiated. Thus it actually classifies urgent updates and slow updates.
If the transition is interrupted by the user actions, React will throw out the stale rendering work that hasn’t yet finished and will render only the latest update.

— Thanks :)

--

--

Pooja Mishra

🌱 Educator 💻 Programmer 🌐 Full Stack Developer 🔥 Motivator 📘 Content creator 🧨 AI 🔥 Machine Learning 👋 ReactJS 🐍 Python ⬆️ Node JS 📈 Entrepreneurship