Skip to content
HomePagesHomePages template kit
Menu

template-kit/single-react

The template-kit/single-react authoring rule — what it enforces, why, and how to fix it.

Exactly one copy of react and one of react-dom in 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.

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:

  1. npm dedupe — if the versions are compatible, npm hoists them to a single copy and you are done.
  2. Align your own range. The kit’s peer range is react@^19 / react-dom@^19; if your package.json pins something older, npm has no choice but to install two.
  3. 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.

package.json
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"legacy-carousel": "^2.1.0" // depends on react@^18
}
}
$ npm ls react
template-workspace
├─┬ legacy-carousel@2.1.0
│ └── react@18.3.1 ← a second React, nested
└── react@19.1.0

legacy-carousel is replaced with a package whose peer range accepts React 19:

package.json
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"swiper": "^12.0.0"
}
}
$ npm ls react
template-workspace
└── react@19.1.0
  • 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.