Stud2design LogoBlog

React 18 features, all that you should know.

publish date

22 May, 2023

read time

8 mins

coverSource: https://unsplash.com/@halacious

In March 2022, the React team released version 18, a major update that brought with it a host of new features. Among these features were significant performance improvements, as well as a focus on introducing new features powered by the new concurrent renderer. While the new concurrent features are opt-in, there is no longer a dedicated concurrent mode.


Despite being more than a year since the release of version 18, many developers are still not fully familiar with all of the changes that were made. In this revision, we will explore the new features that were shipped with version 18, and discuss how they impact developers and ultimately, end users. By taking a closer look at the changes that were made in version 18, we can gain a better understanding of how to leverage these new features to create better, more performant applications.

New Client APIs for React Root.

In React, a “root” is a pointer to the top-level data structure that React uses to track a tree to render, i.e React take over managing the DOM inside it, like listening to state updates and re-rendering and committing the updates.

Before v18, we were passing the React root and a DOM node to render API exposed by react-dom, where this DOM node would become the container node for React root and children to render.

1// Before v18
2import { render } from 'react-dom';
3
4const domNode = document.getElementById('root');
5render(<App />, domNode);

createRoot

With the new createRoot API from the React 18, React will return to us the root for displaying content inside a browser DOM element. The root will have two methods: render and unmount.

1import * as ReactDOMClient from 'react-dom/client';
2
3function App() {
4 return (
5 <div>
6 <h1>Hello World</h1> // [!code highlight]
7 </div>
8 );
9}
10
11const rootElement = document.getElementById('root'); // [!code --]
12
13const root = ReactDOMClient.createRoot(rootElement); // [!code ++]
14root.render(<App />);

To display the UI, we need to call the render method of the root at least once, passing the React component we want to render.


Note ✍🏽The behaviour for the first time you call `root.render` is same as before, React will clear all the existing HTML content inside the React root before rendering the React component into it.

hydrateRoot

We now have hydrateRoot for server-side rendering. The new hydrateRoot API accepts a domNode to which we want to "attach" the React. The domNode should be present in the HTML that was rendered on the server. hydrateRoot accepts the initial JSX (React root) as the second argument.

1import { hydrateRoot } from 'react-dom/client';
2
3const domNode = document.getElementById('root');
4hydrateRoot(domNode, <App />);

hydrateRoot also returns an object with two methods: render and unmount, unlike createRoot, you don’t have to call render function here, as in server response the initial content was already rendered as HTML.

Both createRoot and hydrateRoot also accepts optional options onRecoverableError and identifierPrefix


Render Callback

Before v18, in React we had an optional callback function, if passed to the render API, React would call it after our component is placed into the DOM.

1// Before v18
2import { render } from 'react-dom';
3
4const domNode = document.getElementById('root');
5render(<App />, domNode, callback);

With v18, you can use ref callback

1import { createRoot } from 'react-dom/client';
2
3function App({ callback }) {
4 // Callback will be called when the div is first created.
5 return (
6 <div ref={callback}>
7 <h1>Hello World</h1>
8 </div>
9 );
10}
11
12const app = document.getElementById('root');
13
14const root = createRoot(app);
15root.render(<App callback={() => console.log('Rendered or Updated')} />);

Automatic Batching

Batching is when React groups multiple state updates into a single re-render for better performance.

Batching has always been the part of React, React used to only batch updates during a browser event (like click), so if multiple state updates were present in an event handler it would have result in only one render, whereas updates inside the blocks of promises, setTimeout, native event handlers, or any other event were not batched in React by default.


With introduction of automatic batching in React18, React will automatically batch multiple states update. This will decrease unnecessary re-renders and improve performance.

1// Before: only React events were batched.
2setTimeout(() => {
3 setCount((c) => c + 1);
4 setFlag((f) => !f);
5 // React will render twice, once for each state update (no batching)
6}, 1000);
7
8// After: updates inside of timeouts, promises,
9// native event handlers or any other event are batched.
10setTimeout(() => {
11 setCount((c) => c + 1);
12 setFlag((f) => !f);
13 // React will only re-render once at the end (that's batching!)
14}, 1000);

Also with help of automatic batching we don't have to handle race conditions like where we update response and loading states, earlier when doing so, we have to merge both of them into one object, now we can avoid that as React will only renders once for both updates.


Opt out of batching, some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync()

1// Note: react-dom, not react
2import { flushSync } from 'react-dom';
3
4function handleClick() {
5 flushSync(() => {
6 setCounter((c) => c + 1);
7 });
8 // React has updated the DOM by now, i.e render and commit is done.
9
10 flushSync(() => {
11 setFlag((f) => !f);
12 });
13 // React has updated the DOM again by now.
14}

Concurrent Features

Till now we have been working with React where one component can render at a time, and if a component is rendering then it will block the main thread and no other action like user input can be performed.


To solve this performance issue and user experience, React team have been working on it for some time now. In idle JS execution we tend to move such blocking tasks(say computation work) to a worker thread and avoid the blocking of main thread, React tried workers and other concept to handle multiple task at the same time but it didn’t helped React much.


What React team realised that rather than having all things working in parallel they need a solution where they can prioritise these tasks and execute them based on the priority and switching between them rather synchronously. Thats when they introduced the Fiber.


React started working on concurrent features with React 16.3, at that time they called it “Async Rendering”, like async function, the component rendering will be non blocking and not on main thread.


Tip 👀When talking about concurrent rendering, the mental model should be that rendering is not updating DOM UI, in React that is commit, while rendering the React is evaluating the new tree. You can read more on React rendering here, https://react.dev/learn/render-and-commit

In React v18, the concurrent rendering is a behind-the-scenes mechanism where rendering can be paused or stopped in between and can be restarted from where it left off. This means that if some other task needs to be performed by main thread while a component is rendering, it won't affect the rendering process and the rendering will continue from where it was left off. Although at a time there will be only one component rendering but we would be able to switch between the rendering of the components.


Concepts for concurrent rendering :


WIP 🚧With Concurrent React we shall be able to remove sections of the UI from the screen, then add them back later while reusing the previous state.

Introduced concurrent Features:

React no longer supports the concurrent mode, now with v18 concurrent rendering is opt in, if you use concurrent APIs/hooks then concurrent rendering will be enabled for you in that part of UI.


useTransition

With this hook, React can update the state without blocking the UI, these updates are referred to as transition. useTransition returns an array of two elements, first isPending which can be used for status of the transitions, and second startTransition function which takes a callback as argument with all the set calls(state updates) which need to be transitions.

1import { useTransition } from 'react';
2..
3..
4const [isPending, startTransition] = useTransition();
5
6// Urgent: Show what was typed
7setInputValue(input);
8
9// Mark any state updates inside as transitions
10startTransition(() => {
11 // Transition: Show the results
12 setSearchQuery(input);
13});

What is transition ?

Any state update transition the UI from one view to another. Transition are interruptible in nature, say while a previous transition was performing a rendering and again the transition is called with new state value then React will throw out the stale rendering work that wasn’t finished and render only the latest update. Transition let you avoid wasting time rendering content that's no longer relevant.



Note ✍🏽The function passed to startTransition runs synchronously, but any updates inside of it are marked as “transitions”. React will use this information later when processing the updates to decide how to render the update.
1// React internal concept
2
3let isInTransition = false;
4
5function startTransition(fn) {
6 isInTransition = true;
7 fn(); // setState fn
8 isInTransition = false;
9}
10
11function setState(value) {
12 stateQueue.push({
13 nextState: value,
14 isTransition: isInTransition,
15 });
16}

useDeferredValue

With this hook, you can pass a value and React returns a deferred value of it. Then the rendering because of this deferred value will always lag behind, meaning React will always perform a background re-render for the deferred value, and will only commit for the re-render if its not suspended or interrupted by another new value. If the background re-render is interrupted then render from new value will be used for commit.

1export default function App() {
2 const [text, setText] = useState('');
3 const searchText = useDeferredValue(text);
4 return (
5 <>
6 <input id="search" value={text} onChange={e => setText(e.target.value)} />
7 <SearcResult isStale={text!= searchText} text={searchText} />
8 </br>
9 );
10}

In above example if React is in the middle of re-rendering the Search list, but the user makes another keystroke, React will abandon that re-render, handle the keystroke, and then start rendering in background again. We can use isStale to enhance the UX.


startTransition

startTransition is similar to second element returned from useTransition hook. We can use startTransition function in case we don’t want to use isPending or when we want to use transition outside the React scope, like in a data library.


StrictMode

In the future, React team plans to add a feature that allows React to add and remove sections of the UI while preserving state, i.e React would unmount and remount trees using the same component state as before.

This requires the components to be resilient to effects being mounted and destroyed multiple times, also your components are Pure in nature, the React components you write must always return the same JSX given the same inputs (props, state, and context).

To make sure of that our components are Pure in nature React team has come up with StrictMode component, for all the components wrapped inside <StrictMode> React will simulate un-mounting and remounting the component in development mode.

1import { StrictMode } from 'react';
2import { createRoot } from 'react-dom/client';
3
4const root = createRoot(document.getElementById('root'));
5root.render(
6 <StrictMode>
7 <App />
8 </StrictMode>
9);

Strict Mode enables the following development-only behaviors:


Suspense

Suspense as we know was introduced several years ago. However, the only supported use case then was code splitting with React.lazy, and it wasn’t supported at all when rendering on the server. With React v18 Suspense come with many under the hood benefits. The Suspense component is basically used to show fallback UI while a child component is in suspended state.

1<Suspense fallback={<AlbumsGlimmer />}>
2 <Albums />
3</Suspense>

Suspense, when combined with concurrent APIs, provides the optimized user experience and performance. When using Suspense with concurrent APIs, the Suspense component can be used to avoid showing fallback content again and instead show stale data only.


✍🏼 What is Suspended Component ?


When a child component suspends, the closest parent Suspense component shows the fallback. This let us nest multiple Suspense components to create a loading sequence, and show UI to the user as its readily available.


Before v18 when we were using Suspense around a dynamic import and other children, React would render and call effects for suspended components siblings which were not suspended and available for rendering, but would avoid showing them, by hiding them using css. Instead React would only show the fallback UI, but with v18 Suspense React wait to commit everything inside the Suspense boundary — the suspended component and all its siblings — until the suspended data has resolved. Every time when a child component is suspended again React will clean up layout Effects in the content tree. When the content is ready to be shown again, React will fire the layout Effects again.


With v18 now we can use Suspense for the server side rendering also, sorry but we won’t be diving into that in this blog post, as that topic deserves a post of its own.


Final Thoughts

So V18 has brought a ton of new stuff, but let me tell you, this is just the beginning. React has got some big plans for the future, and getting on board with these changes now will totally set you up for what's coming next. Keep your eye out for Server components.

Resources