Upgrading Your Theme And Extensions To React 19
EverShop 2.2.1 upgraded the framework from React 17 to React 19.
Because EverShop resolves React through a single hoisted copy (a webpack alias), your theme and your extensions run on the same React as the core. There is no opt-out: once you upgrade EverShop, your components are React 19 components.
None of the changes below produce a build error. Your theme compiles, the server starts, and the damage shows up at runtime as a blank area, an ignored default value, or a hydration mismatch that only appears in production. Read the whole page before upgrading a live store.
Quick checklist
Search your theme and extension source for each of these:
| Search for | Status | Fix |
|---|---|---|
defaultProps | Ignored on function components | ES default parameters |
propTypes | Removed from React | Delete, or use TypeScript |
react-toastify | No longer a dependency | sonner, via the core re-export |
ReactDOM.render, ReactDOM.hydrate | Removed | createRoot / hydrateRoot |
findDOMNode | Removed | Refs |
String refs (ref="name") | Removed | Callback refs or useRef |
_( at module scope | Freezes the translation | Call it inside the component |
useState(window...) | Hydration mismatch | Read it in useEffect |
defaultProps is ignored on function components
This is the change most likely to break an EverShop theme, because the component system itself used to rely on it.
React 19 ignores Component.defaultProps on function components entirely. It does not warn at build time. Your component simply receives undefined where it used to receive a default.
// Before — silently broken on React 19
function ProductBadge({ label, tone }) {
return <span className={`badge badge--${tone}`}>{label}</span>;
}
ProductBadge.defaultProps = { tone: 'default' };
// After
function ProductBadge({ label, tone = 'default' }) {
return <span className={`badge badge--${tone}`}>{label}</span>;
}
The Area case
If you previously passed components into an Area by assigning to Area.defaultProps.components, that channel no longer works — and because an Area with no components renders nothing, the symptom is a blank page section rather than an error.
Use the exported helpers instead:
import { setAreaComponents, getAreaComponents } from '@components/common/Area.js';
// The first argument is a ROUTE id, not an area id, and the second is a nested
// areaId -> componentId map:
setAreaComponents('productView', {
productPageTop: {
mySection: { id: 'mySection', sortOrder: 10, component: { default: MySection } }
}
});
setAreaComponents replaces, it does not mergeIt assigns the whole map for that route, clobbering the one the build already
generated. For a theme or extension that just wants to add to an area, the supported
route is still an export const layout = { areaId, sortOrder } in the route folder —
reach for setAreaComponents only when you genuinely want to take over a route's map.
propTypes is removed
React 19 removed propTypes support. Declarations are ignored; nothing validates and nothing warns. The prop-types package is still installed as a dependency of @evershop/evershop — 37 legacy .jsx files in core still import it — but React no longer reads it, so it has no effect on your components either way.
// Before
MyWidget.propTypes = {
title: PropTypes.string.isRequired,
count: PropTypes.number
};
Delete them. If you want the safety back, write the component in TypeScript and type its props — which is what core does now.
Toast notifications: react-toastify → sonner
react-toastify v6 depended on react-transition-group, which used the removed findDOMNode. It has been replaced by sonner.
// Before
import { toast } from 'react-toastify';
// After — prefer the core re-export so you always match the mounted Toaster
import { toast } from '@components/common/ui/Sonner.js';
toast.success('Saved');
toast.error('Something went wrong');
The toast.* call surface is close to drop-in. Two structural differences:
<ToastContainer />becomes<Toaster />.- You almost certainly do not need to mount one at all. Core already renders the
Toasteron the storefront, so importingtoastand calling it is enough.
Rendering APIs
ReactDOM.render and ReactDOM.hydrate are gone. This only affects you if you wrote custom entry/mount code — normal page and widget components are mounted by EverShop.
// Before
ReactDOM.hydrate(<App />, container);
ReactDOM.render(<App />, container);
// After
import { createRoot, hydrateRoot } from 'react-dom/client';
hydrateRoot(container, <App />); // server-rendered markup
createRoot(container).render(<App />); // client-only
Also removed: findDOMNode, string refs (ref="input"), and legacy context (contextTypes / getChildContext). Use useRef/callback refs and createContext.
SSR traps
EverShop server-renders every storefront page and hydrates it on the client. React 19 is far stricter about the two renders producing identical markup — a mismatch that React 17 patched over now discards the server HTML and re-renders on the client.
Never call _() at module scope
The translation helper resolves against the active dictionary, which is per-request on the server and per-page on the client. Calling it at module scope freezes the result at import time, so the server and client can disagree.
import { _ } from '@evershop/evershop/lib/locale/translate/_';
// Broken — evaluated once, at import
const SORT_OPTIONS = [
{ value: 'price', label: _('Price') },
{ value: 'name', label: _('Name') }
];
export default function ProductSorting() {
return <Select options={SORT_OPTIONS} />;
}
// Correct — evaluated per render, inside the component
export default function ProductSorting() {
const sortOptions = [
{ value: 'price', label: _('Price') },
{ value: 'name', label: _('Name') }
];
return <Select options={sortOptions} />;
}
This applies to any module-scope const — arrays, objects, and default parameter values.
Read client-only state in an effect
window, localStorage and document do not exist during the server render. Reading them in a useState initializer makes the first client render differ from the server's.
// Broken — server renders '', client renders the real URL
const [current, setCurrent] = useState(window.location.search);
// Correct
const [current, setCurrent] = useState('');
useEffect(() => {
setCurrent(window.location.search);
}, []);
Dependency notes
@types/reactmust resolve to v19.@types/react@18againstreact@^19produces JSX type errors in a theme'stscbuild.- CKEditor packages (
@ckeditor/ckeditor5-build-classic,@ckeditor/ckeditor5-react) were removed from core. If your extension imported them through EverShop, add them to your ownpackage.json. - Node.js 20 or newer is required. EverShop is tested on Node 20 and 22.
Verifying the upgrade
A clean build proves almost nothing here, so check behaviour:
- Open a storefront page and watch the browser console. Hydration mismatches are logged as errors. Core installs a root error boundary plus
onRecoverableErrorreporting, so a mismatch is reported rather than silently swallowed. - Load a page in a non-default language. Module-scope
_()calls surface here first. - Look for empty areas. A section that renders nothing is the signature of a lost
defaultPropscomponent channel. - Trigger a form submission. If a toast never appears, an old
react-toastifyimport is still in the tree.
See also
- Theme Overview — theme structure and the override seams
- View System — how components compose into pages via Areas
- Templating — overriding and extending components
