template-kit/single-react
The template-kit/single-react authoring rule — what it enforces, why, and how to fix it.
Exactly one copy of
reactand one ofreact-domin the installed tree. A second copy breaks hydration only in production — never in your preview.
The installed tree may contain one react and one react-dom. A nested duplicate
anywhere under node_modules is a violation, whoever pulled it in.
React is a peer dependency of the kit, not a bundled one: the kit ships against the React your workspace installs. So do your islands, and so does every React package you add. They all have to be the same React.
Reason
Section titled “Reason”React’s hooks are not self-contained. useState does not hold your state — it dispatches
through a module-level dispatcher that the currently-rendering React sets. Two copies of
React on a page means two dispatchers, and an island rendered by one while hydrated by the
other reads a dispatcher that is null or foreign. Hydration throws (Invalid hook call is
the usual spelling), the island never comes alive, and whatever it was — the filter, the
carousel, the lightbox — is inert on the page.
What makes this the worst kind of bug is where it shows up. Islands are hydrated on the
published page. In the editor, an island renders as static markup unless it opts into
live: true, so it is never hydrated and the two Reacts never meet. Your preview is
perfect. Your submission builds. The section is dead on the customer’s site, and nowhere
else.
Get the tree back to one copy:
npm dedupe— if the versions are compatible, npm hoists them to a single copy and you are done.- Align your own range. The kit’s peer range is
react@^19/react-dom@^19; if yourpackage.jsonpins something older, npm has no choice but to install two. - Deal with the package that pinned its own. A dependency that depends on a React version range your React does not satisfy will always get its own nested copy. Widen its range if it has a newer release, or replace the package. There is no way to make two Reacts coexist.
Confirm with npm ls react react-dom — one entry each, no deduped-less nested copies.
Before
Section titled “Before”{ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "legacy-carousel": "^2.1.0" // depends on react@^18 }}$ npm ls reacttemplate-workspace├─┬ legacy-carousel@2.1.0│ └── react@18.3.1 ← a second React, nested└── react@19.1.0legacy-carousel is replaced with a package whose peer range accepts React 19:
{ "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "swiper": "^12.0.0" }}$ npm ls reacttemplate-workspace└── react@19.1.0See also
Section titled “See also”- Islands — what gets hydrated on the published page, and what does not get hydrated in the editor.
lockfile-stale— the other check that reads your installed tree.