React 17

Updating React to version 17

Sho Mukai
TeamForm
Published in
3 min readDec 31, 2020

--

While React.js version 17 doesn’t have any new features, there are a lot of changes under the hood some of which are breaking changes. The changes and the reasoning behind them are well described in the official React blog post.

Having recently performed an upgrade from version 16 to 17 and would like to share some of my findings and learnings that hopefully help in your upgrade.

Event Listeners
One breaking change is event listeners. Previous versions of React attach a single event listener to the document element using their own event dispatch system inside of it. This saves some memory and makes React portal implementation possible but has issues, for example, stopPropagation() not really stopping the event.

Now in version 17, event listeners are attached to react app mounting root. It solves some big issues and makes rendering the app and handling events inside shadow DOM possible without requiring react-shadow-dom-retarget-events library.

As a result of the change, the common use case for hiding a modal or menu by listening for click event in the document will no longer work if the event handler in React calls stopPropagation().

For example: if we tried to hide the menu on click, in previous versions we could do something like this inside useEffect().

// This custom handler will no longer receive clicks
// from React components that called e.stopPropagation()
document.addEventListener('click', function() {
setVisibleState(false);
});

In version 17 this will now not work, there is however an easy fix where we can just attach a listener in the capture phase and use setTimeout to make the state change after the event is handled.

document.addEventListener('click', function() {
setTimemout(() => setVisibleState(false), 0);
}, { capture: true });

Although I would like to mention, that React event still works the same way as before: if a component is re-rendered before the later event is handled, then the later event will not be fired. For example: if we change the parent component state in the mouseDown event and re-render children, the click event in children will not be fired. That’s why a setTimeout is needed here.

Except for the above, some other changes include the removal of event.persist(). There are many other improvements and they should not be breaking.

react-scripts and Jest upgrade
At the same time, I upgradedreact-scripts to version 4 (which updates Jest to latest version 26) and found some things to watch out for:

  1. Don’t mock Math.random() globally as Jest is running in the same thread as tests. This will affect the internal process like jest-babel and will get some errors, like this one in source-map-support package:
TypeError: Cannot read property ‘generatedLine’ of undefined

The solution is to mock it separately in tests:

beforeEach(() => {
jest.spyOn(global.Math, “random”).mockReturnValue(0.42);
});
afterEach(() => {
jest.spyOn(global.Math, “random”).mockRestore();
});

2. describe and it should be imported from @jest/globals as the jasmin.currentEnv_ no longer exists.

3. New way to mock Date.now()

jest.useFakeTimers("modern").setSystemTime(new Date("2020-12-31").getTime());

4. react-scripts jest config has a breaking change: resetMocks is true by default and also causes mocks to reset in nested describe. To make jest config backwards compatible, you will have to override the default value.

react-refresh improvements
After updating to react-scripts version 4, the react-refresh package is working pretty well. No longer needing browser refreshes (hot reloading) to load the new component — changes are loaded without losing the current state. :)

Happy new year!

--

--

Sho Mukai
TeamForm

Coder :) @TeamForm — helping teams to thrive