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:
- Don’t mock
Math.random()
globally asJest
is running in the same thread as tests. This will affect the internal process likejest-babel
and will get some errors, like this one insource-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!