The SonarQube interface is written in React and we recently went through the process of upgrading from version 17 to 18. To give you a bit more of the picture, the app is also written in TypeScript and uses Jest with React Testing Library (RTL) for testing.
We wanted to share the biggest three issues we faced and the lessons we learned as we carried out the upgrade. In brief, they were:
- Some TypeScript types changed
- React Testing Library must also be updated
- React 18 brings breaking changes
Let's get into what these meant and how we dealt with them.
Note: this post was co-written by the SonarQube front-end team of David Cho-Lerat, Ambroise Christea, and Philippe Perrin.
The React 18 upgrade guide points out that both
@types/react-dom must be updated as you upgrade, and the "most notable change is that the
children prop now needs to be listed explicitly when defining props."
You can run the codemod using npx like so:
It will present a number of transforms you can apply and will default to the transforms that are required.
For example, the transform to list the
children prop explicitly will take a component that looks like this:
and replace it with:
Watch out though, we found that the codemod can end up nesting the
PropsWithChildren type and your type might end up looking like this:
While this isn't harmful, you will want to correct these types as you come across them.
The new types are also more picky in some areas. For example, previously we were able to override the type of
children in an interface like this:
With the React 18 types, this no longer works and you must now omit the declaration of
The new types also don't allow implicit
any types for the parameters to a
useCallback function. You will need to explicitly declare the types, for example:
When you upgrade
@types/react to version 18, expect to see a few issues like this.
We found that many of our tests that used to pass now failed after updating React and RTL. There were two categories of failure: timing and calls to
In version 14.1.0, RTL added an
advanceTimers option to the setup step for user-event so that you can provide your own timer. We were able to fix our tests by passing the
act(...) warning had plagued our codebase for a while and in some cases had been patched up by adding an extra call to
act around some RTL events and helpers.
RTL helpers use
act internally, so while adding an extra call to
act was initially a valid workaround to suppress the warning, it now caused the tests to fail. Removing the excess calls to
act got the tests passing again. If you still receive warnings, Kent C. Dodds has a comprehensive post on what causes the act(...) warning and how to fix it in the context of RTL.
The biggest change in React 18 is right at the root of the application.
ReactDOM.render is no longer supported and should be replaced with
createRoot. While on the surface this seems like a simple change that provides a better way for React to manage the root of the application, it actually changes how React renders your application. Two new features are enabled: automatic batching and the new concurrent renderer.
Concurrent rendering allows React to interrupt the rendering of a component if there is other work that needs to be done at a higher priority. You opt-in to this behaviour by defining a state update as a transition using the
useTransition hook. If you don't opt-in, your components will render sequentially as before, so this should make no difference as you upgrade your application.
However, automatic batching is enabled immediately. Automatic batching is a performance improvement in React 18 to reduce the number of renders by collecting state changes into one update. It can cause some unexpected behaviour though.
We discovered some parts of our code fell foul of this new batching when several tests started failing. The tests were expecting parts of components to be rendered, yet found them to be empty.
We realized that this batching includes any execution sub-context in the same scope! This means that if you have a
setState, then a Promise that also does a
setState when it resolves/rejects, both state changes will be batched at the end of the scope if they happen close to each other (for instance in tests, where mocked queries are almost instantaneous).
In this simplified example of two methods in a class-based component we set a state, then, within the body of an asynchronous function, we relied on that new state in a conditional.
In React 17 when
handleFetchProjectsClick was called it would set the
shouldFetch state to
true, then call on
fetchProjects the test for
shouldFetch would be
true and the data was fetched. This is because the state update task happens before the
fetchProjects promise is handled.
In React 18 with
createRoot the projects aren't fetched because the state update is deferred until the end of
handleFetchProjectsClick, so when
shouldFetch would still be falsy.
If you need to ensure code runs after state is set, you can either use the callback form of setState or the new
The above fixed the component rendering on the page, but we found that the tests continued to fail. Debugging these failures step-by-step showed that the content was not present on the initial render, but when we re-rendered the component it then appeared. Because we now used the
setState callback method to fetch the data, the initial render didn't include the content.
Our tests were using RTL's synchronous methods to find that content on the page, like this:
Replacing RTL's synchronous
getBy queries with the asynchronous
await findBy query fixes the issue. For example:
The SonarQube UI is now running successfully on React 18. While some of the issues we came across had to do with the test suite needing an upgrade, others were caught because we have a comprehensive test suite across both unit tests for components and end-to-end tests avoiding production failures when it was time to deploy. Writing testable code is one part of writing adaptable code, one of the properties of Clean Code. Those tests highlighted things that needed to be updated and gave us the confidence that when they passed, the application was ready.
If you are running React 17 and planning an upgrade, hopefully, these experiences can help you with some of the pitfalls.