It’s a lovely workday morning and the Slack notification chime takes me away from my morning tea. This time it’s ‘Pineapple & Pear’, and as I hear tea sommeliers moaning at such a travesty, I see a message from a colleague:
Could you check out what’s new in React 18 and update the X project if possible?
I used to dabble in the fine art of frontend-craft (a distant, slightly awkward cousin of witchcraft), so that shouldn’t be an issue, I assume, and begin my journey into ‘what’s new in React 18’…
The mundane of keeping up
Every time I see a full URL containing the date stamp, I unconsciously make an assumption about how stale the data I’m about to look into is. And with technology-related articles, blog posts, and changelogs in this day and age, the period for ‘still-not-stale’ is getting shorter and shorter.
As your favourite packages are bumping their versions almost every day, it becomes a mundane ‘not staying behind’ cat and mouse game.
One could simply upgrade all the packages to the latest non-major versions (following Semantic Versioning specification, which would mean updates that do not contain incompatible API changes). But, as recently reiterated by the incidents with highly-used OSS packages, it might not be the best option for you or your client’s project.
As with everything in software development, choosing when and what to update is a balancing act between business needs and technical debt. And that’s a decision every tech team has to make and dedicate time for.
The fright of checking
Depending on the number of external packages your project relies on, running
npm outdated or
yarn outdated can be a fun little task before your stand-up or a stressful day spent in changelogs.
npm outdated will provide you with a list of the packages used in the project, their current version, wanted version (the version that can be automatically updated based on package ranges defined in package.json using npm update), and the latest version of the package.
An example excerpt from one of our projects was:
An alternative tool to manage your package versions for npm packages is
ncu – npm-check-updates. Yarn also offers interactive tools with the
yarn interactive upgrade.
Now that the situation is clear, the real investigation can begin: what’s changed, how it will affect the project, and what changes are required. With a few exceptions, patch and minor version updates shouldn’t create any issues, however, it’s still a good idea to check what’s changed in the minor updates. It’s also important to check how new the changes are and whether these changes have introduced any issues.
The fun of catching up
As React was bumped to a major version, we can safely assume that something will need to be changed in code to support the new version.
In React, a ‘root’ element is a reference to the top-level data structure point from which the component tree is tracked and rendered. Before version 18, this element wasn’t accessible to developers as it was directly attached to the DOM element and accessed through the DOM node. Developers would get the DOM element and pass it into the
render() function. Now, the root element is created first, and then the rendering can be performed from it.
React 18 ships with support for both legacy and new root APIs, which allows users to:
- gradually update to the new API without the application crashing on update
- more easily test the performance of both approaches.
A warning to the legacy root API has been added that recommends using the new API. And to update, we will need to start using
react-dom/client instead of
is replaced with
Automatic batching is a process React uses to automatically batch state updates in React event handlers that limits the number of renders needed to update the view to its current state. With React 18, this optimisation is extended to promises, setTimeouts, and native event handlers – and re-render happens only once where possible.
An example for autobatching is attached below. As React 18 ships with both Root APIs, you can easily switch between them and see the difference in re-rendering using React Dev Tools Profiler, as new functionality is only available with the new Root API.
Clicking the Event Handler button fires a regular event handler, and both React 18 and the previous implementation will re-render once. Clicking the Fetch or Timeout buttons would force two re-renders in the previous React version, but with React 18, setting the state is batched and re-render only happens once:
Example code also includes the
flushSync() method, which disables automatic batching if needed. Both Root APIs will re-render twice when running the
With React 18 comes a new way to render content in React: the concurrent render. The key property behind it is that rendering is interruptible. With previous React versions and with React 18, without any concurrent features, updates are rendered in a single, uninterrupted transaction – once it starts, it can’t be stopped until it’s finished.
With concurrent rendering, React can interrupt – stop or pause – rendering the updates. DOM mutation is left until the last moment when the entire tree has been evaluated.
A lot of new React will be based on concurrency support.
React 18 has introduced new concepts and different types of updates – urgent and transitional updates.
Urgent updates are usually instances whereby we have a natural expectancy of how and how fast they should work. Pressed key should be shown in an input field immediately if possible. Results from search based on the input value aren’t expected immediately, so such updates aren’t considered urgent and could be labelled as transitional updates.
Transitional updates are marked using the
startTransition() method or
Every React developer has had to deal with a loading state at some point – displaying a spinner while waiting for something to load.
To help with that, there is Suspense. It lets you declaratively specify the loading state for a part of the component tree if it’s not yet ready to be displayed:
With Suspense, your component can suspend its rendering until ready. When React notices such a state, it will traverse up the tree to the nearest Suspense component and will display the value from fallback while the component is getting ready.
With React 18’s Suspense, two major SSR features are added:
- HTML streaming
- Selective hydration
With HTML streaming, by wrapping components in , we can tell React that it doesn’t need to wait for the wrapped component to start streaming the HTML. React will send the placeholder (Suspense’s fallback).
With selective hydration, we can wait for a component of code to load without blocking the rest of the page – hydration can start before all of the code is loaded. Now
React.lazy() works with SSR.
Hydration is a process of rendering the components and attaching event handlers. It’s important when using Server Side Rendering where non-interactive (sometimes called ‘dry’) HTML and CSS are rendered on the server and sent to the client. After that, the ‘hydration’ of ‘dry’ content happens, after which users can interact with the content. It’s useful because users can actually see some content even before they can interact with it, making the whole experience a lot smoother.
hydrateRoot() method from
react-dom/client has been added to hydrate a server rendered application.
To finalise the React 18 migration, some additional changes were required in tests.
act() was introduced to React with hooks as
useEffect() was starting to schedule asynchronous work. When testing components with
act(), sometimes asynchronous effects wouldn’t fire, and tests wouldn’t match production behaviour.
As in concurrent React 18, a lot more work is happening asynchronously, and a lot more situations require
act(). Now, React will return a warning if it receives an update that wasn’t wrapped in
This led to a few tests displaying warnings about missing
act() in our codebase, but they are easy to fix as it just requires wrapping a few fired events.
The disheartening of peer-dependant
“It’s turtles all the way down” is an ancient expression about how much packages depend on each other. And for that reason, it’s sometimes not possible to fully migrate to a newer version of one package because it’s not yet supported by other packages used in the project.
Package developers will start working on supporting new versions of packages their package relies on before official release is ready. For example,
firstname.lastname@example.org was released on 29th of March, 2022 and
email@example.com followed right after and was released on 31st of March, 2022 with a very clear changelog message:
“BREAKING CHANGES: – Drop support for React 17 and earlier.“.
But not all package maintainers have enough resources to provide support for the latest versions of libraries that you would like to use. And thus, we wait…