What’s new in React 18?

Abhishek Gautam
5 min readJun 16, 2021

And it’s huge this time, Get Ready!

React 18 changes are huge as it is focused on user experience and internal architectural changes. React 18 adapts an opt-in adoption strategy, which will enable devs to upgrade to React 18 with minimal or no code changes.

Installing React 18 & Upgrading

To install the latest React 18 alpha, use the @aplha tag:

npm install react@alpha react-dom@alpha

The official specific dates are not out yet but the release will happen in this order: React 18 Library Alpha → React 18 public beta → React 18 RC → React 18.

Upgrading on the Client

To upgrade to React 18 after making changes to your package.json you need to update the following in your codebase as now there is a new Root API.

ReactDOM.render(<App />, container);TO THIS : const root = ReactDOM.createRoot(container);
root.render(<App />);

If you previously used hydrate, you'll switch to hydrateRoot.

Upgrading on the Server

To be able to unlock the new React 18 features on the server we will have to switch from renderToString to pipeToNodeWritable. As suspense wasn’t at all supported on the server but things are changing in React 18, though renderToString will continue working but with limited suspense support. So pipeToNodeWritable is the new and recommended way.

New Suspense and SSR Architecture

React 18 includes architectural React SSR performance. Most optimizations are behind-the-scene but there are some opt-in mechanisms as well.

SSR in React happens in several steps:

  • On the server, fetch data for the entire app.
  • Then, on the server, render the entire app to HTML and send it in the response.
  • Then, on the client, load the JavaScript code for the entire app.
  • Then, on the client, connect the JavaScript logic to the server-generated HTML for the entire app (this is “hydration”).

Problem?

Each step had to finish for the entire app before the next step could run but this is not efficient. There is a “waterfall” here: fetch data (server) → render to HTML (server) → load code (client) → hydrate (client).

When suspense was introduced for components in 2018 it only supported Lazy loaded but the goal was to integrate it with SSR to solve this waterfall problem.

Due to the addition of suspense, two major React SSR features are introduced: Streaming HTML and Selective Hydration.

Streaming HTML

This enables the server to send pieces of your components as they get rendered. This works by using Suspense, where we define which parts of our application will take longer to load and which ones should be rendered directly.

<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>

If you see in this example of a post with comments, where the post itself is the critical part of the page, you could say load the post but don't wait for the comments to be ready to send HTML to the browser.

Selective Hydration

We can send the initial HTML earlier, but we still have a problem. Until the JavaScript code for the comments widget loads, we can’t start hydrating our app on the client. If the code size is large, this can take a while. In such a scenario you could implement code splitting by using React.Lazy but this did not work with SSR.

With the new changes to Suspense, now the components wrapped with Suspense won't block the hydration anymore.

Automatic Batching

Batching is whenever react combines multiple state update into a single re-render for better performance. Example:

function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
setCount(c => c + 1); // Does not re-render yet
setFlag(f => !f); // Does not re-render yet
// React will only re-render once at the end (that's batching!)
}

return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}

This works great, instead of re-rendering twice, the component will be rendered only once after the handleClick is called.

Problem?

React wasn't consistent about batch updates. React used to only batch updates during browser events like click, but when we are updating the state after the event has been handled such as in promise or a callback like :

// Promise
fetchSomething().then(()=> {
setCount(count+1);
setFlag(true);
})

//callback or timeout
setTimeOut(()=>{
setCount(count + 1);
setFlag(true);
})

React won't batch these two updates into one and you will get two re-renders when only one would have been needed.

Starting from React 18 with createRoot, all updates will be automatically batched, regardless of where they originate from. This means that updates inside of timeouts, promises, native event handlers, or any other event will batch the same way as updates inside of React events.

How to opt out?

Usually, batching is safe, but some codes may depend on reading something from the DOM immediately after a state change. For those use cases, you can use flushSync to opt-out of batching:

import {flushSync} from 'react-dom';

function handleClick(){
flushSync(()=> {
setCount(count + 1);
});
// React has re-render

flushSync(()=> {
setFlag(true);
});
// React will re-render
}

Transitions

Building apps that always feel responsive and fluid is challenging as small actions like clicking a button or entering some text may make a lot happen in the background this may make the page freeze or hang until the work is done.

Let’s take an example of search box, when the user is updating the search input, the value needs to change as the user is typing. Although the result are only meant to appear once the user is done typing.

Until React 18, all updates were treated as urgent updates. To solve this issue React came with transitions. With the new startTransaction API we can mark updates are ‘transitions’.

Updates wrapped in startTransition are handled as non-urgent and will be interrupted if more urgent updates like clicks or keypresses come in. If a transition gets interrupted by the user (for example, by typing multiple characters in a row), React will throw out the stale rendering work that wasn’t finished and render only the latest update.

Transitions lets you keep most interactions snappy even if they lead to significant UI changes. They also let you avoid wasting time rendering content that’s no longer relevant.

import {startTransition} from 'react';

//Urgent : Update input value as type
setInputValue(input);

startTransition(()=>{
//Secondary: Show the search results
setSearchQuery(input)
});

What to do while the transition is pending?

As a best practice, you’ll want to inform the user that there is work happening in the background. For that, we provide a Hook with an isPending flag for transitions:

import { useTransition } from 'react';const [isPending, startTransition] = useTransition();

The isPending value is true while the transition is pending, allowing you to show an inline spinner while the user waits:

{isPending && <Spinner />}

React 18 isn’t production-ready yet. It’s few months until React 18 will go into public beta and then several weeks after that for stable release so get ready.

Thank you for reading. If you really learned something new with this article, save it and share it with your colleagues.

Happy Learning!

--

--

Abhishek Gautam

A tech geek who loves to solve problems and coming up with meaningful, simplistic solutions.