How to update from React 17 to 18

If you have a large and somewhat aged codebase, it can be quite a journey to perform a React upgrade. Having gone through one of those recently, here are some things I learned when upgrading React 17 to 18.

First things first, there are some new features introduced in React 18 that might introduce breaking changes to your application or tests that are worth knowing about ahead of time. One of the things I wish I had done before making any changes was to give those changes a good read.

Automatic batching

Updates inside Promises, setTimeout, and event handlers are now batched. To understand this, I found the following examples that demonstrated how state changes are handled.

React 17: when multiple states change, they are batched, and one update takes place

React 17: when multiple states change take place after another event is handled, those state changes are not batched

React 18: when multiple states change take place after another event is handled, those state changes ARE batched

startTransition()

There are a number of good user cases for this improvement, the one that seems most relevant for most applications is the following.

Previously, if a user clicks on a button that renders a process, and immediately changes their mind and clicks on another button, there is a momentary freeze while the app renders the slow process.

In the new situation, the state update is marked as a transition, and the slow process does not freeze the page.

Suspense on server

Using the new component Suspense, we can now defer the loading of slow components by wrapping the slow components inside it, replaced with loading state. This seems particularly pertinent to any heavy applications that struggles with lengthy loading time.

https://www.youtube.com/watch?v=pj5N-Khihgc&ab_channel=ReactConf2021
https://www.youtube.com/watch?v=pj5N-Khihgc&ab_channel=ReactConf2021

Strict Mode behaviour

This can be another potentially breaking change for your application. The new Strict Mode mounts, unmounts, and remounts every component in DEVELOPMENT (React 17 apparently does this too, but suppressed the logs). Note that this does not affect anything in production, and is introduced to test the resilience of your application against multiple mounts and dismount, pave the way for possible changes in that direction for production in a future release.

This means that in development, React will simulate create, destroys, and create the states again during the loading. Problems usually come with states that are created but not properly destroyed in useEffect, and might introduce bugs into your tests when combined with some of the other above mentioned concurrency features the React 18 introduces.

New hooks

  • useId()
  • useTransition()
  • useDeferredValue()
  • useSyncExternalStore()
  • useInsertionEffect()

Here's a step by step run-down of what you should do during the upgrade.

  1. Upgrade react, react-dom, types
  2. Replace `ReactDOM.render` with `createRoot` to unlock React 18 concurrency features
  3. Update any library specifically related to a previous React version. For example, I had to find a replacement Enzyme adapter libary to replace an older version that only served React 17.
  4. Update any libraries with dependencies to the older version of React.

One of the issues that arose in the tests was related to what I suspect to be React's concurrency changes. Tests that required component mounting and then checking against snapshots or the correct actions getting dispatched, might start to fail without wrapping them in either act() or waitFor().

There might also be adjustments needed to ensure that certain actions are dispatched only once when useEffect runs.