React 18 New Features
React 18 RC is out, which means the official stable release is close. I think it may be a good time to recap some of the important features in this big update.
There are 3 big features and improvements in React 18:
- Automatic batching
- Concurrent rendering
- SSR features & improvements
Automatic Batching
First, let's talk about the improvements of a feature called automatic batching.
In fact, we already have automatic batching in React 17.
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React will only re-render once
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? 'blue' : 'black' }}>{count}</h1>
</div>
);
}
In the example code above, when the handleClick
function is called, even though we update 2 states together (setCount
and setFlag
), React will only re-render once instead of twice. This behavior is called automatic batching.
However, until React 18, it only batches updates during the React event handlers like onClick
in <button>
. This means that if you call multiple setState
functions in other places like setTimeout
, fetch().then
or addEventListener
, they won't be automatically batched.
This is no longer the case in React 18. In all the following places, setState
s will be automatically batched since React 18.
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
}, 1000);
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
});
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
});
Since React 18 does automatic batching for you, the old unstable_batchedUpdates
API which was used to achieve this will be removed in future versions.
Also, in case you don't want automatic batching, you can use a new API called flushSync
to prevent this behavior.
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
flushSync(() => {
setFlag(f => !f);
});
// React will re-render twice
}
Read the discussion about automatic batching in the React 18 Working Group for more details.
Concurrent Rendering
In React 18, several new concurrent features are introduced. Here I will talk about the only documented API startTransition
.
React 18 classifies state updates in two categories:
- Urgent updates: direct interactions like typing, clicking, pressing and so on
- Transition updates: the state updates that are not that urgent and can be interrupted by urgent updates
This graph by Dan Abramov explains the difference between urgent and transition updates well.
In this example, we have an urgent update setInputValue
and a transition update setSearchQuery
. During the execution of setSearchQuery
, if setInputValue
is called, then setSearchQuery
's execution will be interrupted and setInputValue
will be prioritized.
In React 18, every state update is an urgent update by default. To make an update a transition, we can use the startTransition
API.
import { startTransition } from 'react';
setInputValue(input); // Urgent update
startTransition(() => {
setSearchQuery(input); // Transition update
});
Read the discussion about startTransition
in the React 18 Working Group for more details.
There are other concurrent feature APIs like <SuspenseList>
or useDeferredValue
, but because there are no document about them yet I will skip them here.
SSR Features & Improvements
In React 18, SSR (Server-Side Rendering) gets a lot of new features and improvements.
For improvements, <Suspense>
and React.lazy
are finally supported in SSR.
For new SSR features, there are two major features:
- Streaming HTML on the server
- Selective Hydration on the client
Streaming HTML on the server
Before React 18, there is a waterfall process before the user can interact with the app, and this waterfall is a process for the whole app:
- Fetch data (server)
- Render the HTML (server)
- Load JS (client)
- Hydrate (client)
In React 18, thanks to the <Suspense>
SSR support and the ability to stream HTML, we can break down the SSR waterfall into smaller independent sections to make sure we can render each part of the screen as soon as possible.
To use the new feature, simply wrap the component inside <Suspense>
to create a independent section.
Assuming that we have a blog page which has <NavBar>
, <Sidebar>
, <Post>
and <Comments>
components and the <Comments>
component needs to fetch some data.
<Layout>
<NavBar />
<Sidebar />
<RightPane>
<Post />
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
</RightPane>
</Layout>
When we wrap <Comments>
inside a <Suspense>
,
- React first starts streaming the HTML before the data for
<Comments>
is ready and send the placeholder (<Spinner>
) instead - When the data for
<Comments>
is ready, React sends additional HTML into the same stream
Selective Hydration on the client
By using <Suspense>
, not only the sending of HTML but also the hydration of each component is broken down into independent sections. This means:
- Each section separated by
<Suspense>
will start its hydration as soon as its JS loads - Each section can become interactive wile other section is still hydrating
Also, React 18 can prioritize more urgent hydration based on user interactions.
By default, when we have multiple <Suspense>
boundaries, React will hydrate the Suspense that appears earlier in the tree. However, if the user starts interacting with the other boundary, React will hydrate that boundary because it's more urgent.
Read the discussion about the new Suspense SSR architecture in the React 18 Working Group for more details (NOTE: all the graphs used in this section are from the discussion as well).
How to Enable the new features?
If you are using "pure React" like create-react-app, you will need to both upgrade your client and server codes. Meanwhile, if you are using a library like Next.js, you will need to follow their documentations about how to enable these new features.
Here I will only introduce the case for "pure React".
On the client
On your client codes, replace ReactDOM.render
and ReactDOM.hydrate
with new methods.
For ReactDOM.render
, replace this:
// Before
const container = document.getElementById('root');
ReactDOM.render(<App />, container);
to this:
// After
const container = document.getElementById('root');
const root = ReactDOM.createRoot(container);
root.render(<App />);
For ReactDOM.hydrate
, replace this:
// Before
const container = document.getElementById('app');
ReactDOM.hydrate(<App />, container);
to this:
// After
const container = document.getElementById('app');
const root = ReactDOM.hydrateRoot(container, <App />);
Read the discussion about how to upgrade on the client in the React 18 Working Group for more details.
On the server
On your server codes, use the new ReactDOMServer.pipeToNodeWritable
API.
Here is the comparison of the old an new ReactDOMServer
APIs:
ReactDOMServer.renderToNodeStream
: Deprecated (with full Suspense support, but without streaming)ReactDOMServer.renderToString
: Keeps working (with limited Suspense support)ReactDOMServer.pipeToNodeWritable
: New and recommended (with full Suspense support and streaming)
Read the discussion about how to upgrade on the server in the React 18 Working Group for more details.
Conclusion
With these new features in React 18,
- Automatic batching
- Concurrent rendering
- SSR features & improvements
we as React developers will have more options to provide our users a better and more optimized user experience.
If you want to read more information about React 18, check out all the discussions in the React 18 Working Group repo.
Here is a list that I referenced in this article:
- React 18 introduction
- Automatic batching
- startTransition API
- SSR improvements
- Behavioral changes in Suspense
- Upgrade React 18 on the client
- Upgrade React 18 on the server
And there is also an Explain React concepts like I'm five discussion in the same repo that helped me understand some important React concepts. I highly recommend you guys to have a read.