React 17: The Most Important Boring Release
React 17: The Most Important Boring Release#
"React 17 release is unusual because it doesn't add any new developer-facing features." — React Blog, August 2020
That is how the React team opened their announcement. No new hooks. No Concurrent Mode. No Suspense for data fetching. Nothing that would make your code faster or your components simpler.
The community reacted with a mix of confusion and mild disappointment. Understandable. When you have been waiting for Concurrent Mode for what feels like years, "no new features" is not the headline you want.
But I think the people who were disappointed were looking at the wrong things. When a team with React's track record deliberately ships a "boring" release, something important is happening under the hood. Let me explain why I think React 17 matters more than most people realize.
The Event Delegation Change#
This is the biggest change and the one most tutorials skip because it does not affect how you write components day-to-day.
In React 16 and earlier, React attached almost all event listeners to document. Event delegation — instead of a listener on every DOM node, one listener at the top that catches bubbling events.
// React 16: All events delegated to document
// document.addEventListener('click', reactHandler)
// React 17: Events delegated to the React root element
// rootElement.addEventListener('click', reactHandler)
Why does this matter? I will tell you exactly why, because I hit this bug.
At a previous job we were embedding a React 16 widget inside a legacy jQuery application. The jQuery code called event.stopPropagation() in certain places. Because React 16 delegated to document, stopping propagation in a jQuery handler prevented React's listener from ever seeing the event. Certain React click handlers simply never fired. We spent two days debugging this before finding a Stack Overflow answer that explained the delegation model.
With React 17, each React tree attaches its listeners to its own root container. This makes React trees genuinely isolated from each other and from non-React code.
// React 17 mount
const container = document.getElementById('root');
ReactDOM.render(<App />, container);
// Event listeners are now on container, not document
Not exciting to blog about. But if you have ever embedded React into a non-React page, this is a real fix for a real problem.
Gradual Upgrades: Running Two React Versions Side by Side#
The event delegation change was a prerequisite for something bigger: the ability to run React 16 and React 17 on the same page without them interfering with each other's event handling.
// Legacy React 16 section
ReactDOM16.render(<LegacyApp />, document.getElementById('legacy-root'));
// New React 17 section
ReactDOM17.render(<NewFeatureSection />, document.getElementById('new-root'));
// Each tree handles its own events independently
This is most relevant for large organizations running multiple teams on the same codebase, or for apps that need to gradually migrate without a hard cutover. You probably do not need this today. But the fact that it is possible shapes how React thinks about backward compatibility going forward, and that matters.
The New JSX Transform#
Before React 17, every file with JSX needed this at the top:
import React from 'react'; // required, or JSX wouldn't compile
function ProductCard({ name, price, currency }) {
return (
<div className="product-card">
<h2>{name}</h2>
<p>{currency}{price.toFixed(2)}</p>
</div>
);
}
The reason: JSX compiled to React.createElement() calls, so React had to be in scope. With the new transform, the compiler handles this automatically:
// React 17 with new JSX transform: no import needed
function ProductCard({ name, price, currency }) {
return (
<div className="product-card">
<h2>{name}</h2>
<p>{currency}{price.toFixed(2)}</p>
</div>
);
}
// The compiler generates this behind the scenes:
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
function ProductCard({ name, price, currency }) {
return _jsxs('div', {
className: 'product-card',
children: [
_jsx('h2', { children: name }),
_jsx('p', { children: `${currency}${price.toFixed(2)}` }),
],
});
}
Modest improvement. Slightly smaller bundles. Cleaner files. One less thing to explain to beginners. To enable it:
{
"compilerOptions": {
"jsx": "react-jsx"
}
}
{
"presets": [
["@babel/preset-react", { "runtime": "automatic" }]
]
}
I enabled this in every project the day it became available. Small wins add up.
Breaking Changes Worth Knowing#
React 17 is mostly backward compatible, but a few things changed:
Event pooling is gone. In React 16, synthetic events were pooled and reused. This meant you could not access event properties asynchronously:
// React 16: would log null because the event object was recycled
function SearchBar({ onSearch }) {
function handleChange(e) {
setTimeout(() => {
console.log(e.target.value); // null in React 16!
}, 100);
}
return <input onChange={handleChange} />;
}
// React 17: works fine — no more event pooling
function SearchBar({ onSearch }) {
function handleChange(e) {
setTimeout(() => {
console.log(e.target.value); // "typescript" — just works
}, 100);
}
return <input onChange={handleChange} />;
}
I cannot tell you how many times event pooling bit me in React 16. Good riddance.
onScroll no longer bubbles. In React 16, onScroll would bubble to parent elements, which does not match browser behavior. Fixed now.
useEffect cleanup timing. Cleanup now runs asynchronously, consistent with how the setup runs. Unlikely to affect most apps but can surface in tests that relied on synchronous cleanup.
My Take#
React 17 is an infrastructure release. The React team is laying groundwork for Concurrent Mode, Suspense, and the gradual upgrade story that will unfold over the next major versions. Not glamorous. But necessary.
There is a chess analogy here that I think about sometimes: not every move needs to be an attack. Some moves are about improving your position, making future moves possible. React 17 is a positional move. The React team is making sure that when they do ship the exciting features — Concurrent Mode, server components, whatever else is coming — the upgrade path is smooth.
The new JSX transform is a genuine quality-of-life improvement. The event delegation fix solves a real problem that anyone building micro-frontends has encountered. And the gradual upgrade story signals that the React team thinks about long-term maintainability, not just new features. I respect that.
If your team is still on React 16, upgrading to 17 is low-risk. The migration guide is short. The main things to audit: any code that accessed pooled event properties asynchronously (fix: remove event.persist() calls, they are now no-ops), and tests that relied on synchronous cleanup timing.
Pitfalls for Teams Upgrading#
Forgetting to update the JSX transform config. Installing React 17 does not automatically enable the new transform. You have to update Babel or TypeScript config explicitly. Many teams miss this step, then wonder why they still need import React everywhere.
Mixing React versions without understanding isolation. Running React 16 and 17 side-by-side is possible but not free. Both get loaded, bundles get larger. This is for gradual migration, not permanent coexistence.
Assuming useEffect cleanup is unchanged. If tests rely on synchronous cleanup, they may start failing. The fix is usually to use act() correctly in tests rather than relying on timing assumptions.
What is next for this blog: we are going back to hooks. useRef is next — a hook that is about much more than DOM references, and I have a feeling it is the most underappreciated hook in the entire API.