facebook / react · Issue No. 37145
Remounting a Host Singleton (<html>, <head>, <body>) removes every attribute from that DOM node, including attributes React never rendered. acquireSingletonInstance clears the node and then reinitialises it from props, so an attribute written by a pre-hydration inline script is destroyed. I hit this in an app with a dark-mode toggle plus a language switcher: switching language remounts <html>, and the data-theme written by the theme script is gone. For the common localStorage / prefers-color-scheme FOUC script the attribute cannot be a React prop at first paint, because it has to be set before React exists on the page. (A cookie-based theme can be a prop, at the cost of opting the route out of static rendering.)
Hydration does not take this path, so a plain page load is correct and only a later remount loses the attribute. Whichever behaviour is intended, the two paths disagree.
This is not fixed by #37112: that PR changed releaseSingletonInstance only. acquireSingletonInstance is unchanged on main and still loops removeAttributeNode over every attribute. #37112 added a gated // @gate TODO test for the acquire path covering the cold acquire (createRoot over a node that already had an attribute); this report is the remount entry into the same function, reachable from a stock app with no imperative setup. It still reproduces on a canary that contains #37112, which isolates acquire as the remaining cause.
React version: 19.2.8 (also reproduced on 19.3.0-canary-96fcba90-20260728). Not a regression as far as I can tell — the loop is present in shipped 19.2.8 and Host Singletons are 19-only.
git clone https://github.com/crazylulu9999/react-singleton-attribute-wipe-repro && npm install && npm run repro — jsdom + React only, no browser and no framework. react/react-dom are pinned to 19.2.8; for canary, pin both to one published build (npm i react@19.3.0-canary-96fcba90-20260728 react-dom@19.3.0-canary-96fcba90-20260728) — the canary dist-tags were out of sync when I checked. Exits non-zero when it reproduces.<html> carries an attribute React does not render — data-theme="dark", written by an inline pre-hydration theme script.hydrateRoot(document, <html lang="ja">…</html>). data-theme survives hydration.<html> element: render <html key="ko" lang="ko">…</html>.data-theme is gone. Only lang remains.TEST 1 — server HTML -> hydrate -> remount
ok after hydration <html data-theme, lang>
LOST after <html> remount (key ja -> ko) <html lang>
Hydration is not required: TEST 2 in the same script client-renders with createRoot, sets data-theme imperatively while React owns <html>, then remounts — same loss.
The explicit key in step 3 stands in for what a router does on a route transition; app code does not normally key <html> by hand (see Impact). Keying a singleton is a contemplated case — commitDeletionEffectsOnFiber calls commitHostSingletonRelease eagerly with the comment "if you keyed a HostSingleton so there will be a delete followed by a Placement".
Remounting <html> is a valid React operation. The bug is that singleton acquire destroys attributes React never owned; the Next.js i18n layout is how apps reach it, not the cause.
npm run browser — real browser, no test harness. Toggle dark mode, then click another language: the page goes from dark to white and stays white.

Link to code example: https://github.com/crazylulu9999/react-singleton-attribute-wipe-repro
acquireSingletonInstance in packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js clears the node unconditionally before reinitialising it from props:
const attributes = instance.attributes;
while (attributes.length) {
instance.removeAttributeNode(attributes[0]);
}
setInitialProperties(instance, type, props);
Hydration goes through prepareToHydrateHostInstance instead: the hydrated branch of case HostSingleton in completeWork never calls markUpdate, and commitHostSingletonAcquisition is gated on current === null && flags & Update, so acquire never runs on a hydrated singleton. That is the asymmetry in the steps above.
data-theme is still on <html> after the remount. An attribute React never rendered should survive a singleton remount, exactly as it survives hydration. #37112 added this comment to releaseSingletonInstance:
Only remove state that was represented by this Fiber's props. Attributes added imperatively while React owned the singleton must be preserved.
The acquire path should follow the same rule. A keyed remount is delete-then-placement, so it runs release and then acquire on the same node: after #37112, release now preserves the non-React attributes and acquire immediately wipes them anyway. Release already removed exactly what React set, so the wholesale clear in acquire is redundant in this case.
The cold acquire is a genuinely different question — there React has no record of what it owns — and this report is only about the remount case, where a prior committed props record exists.
Reachable from the documented Next.js App Router i18n layout, where the [lang] segment is the root layout rendering <html lang={...}>. createRouterCacheKey includes the dynamic segment's value (`${segment[0]}|${segment[1]}|${segment[2]}`) and that key is passed as the React key in layout-router.tsx, while isNavigatingToNewRootLayout deliberately ignores the value — so /ja → /ko stays a soft navigation and remounts <html>, dropping data-theme / class="dark". Framework-level repro: https://github.com/crazylulu9999/nextjs-locale-switch-wipes-html-attributes
Reported downstream since 2023 without the mechanism being identified:
html tag will be remove"next-themes seeds from localStorage and re-applies the attribute in an effect, so its users see a flash rather than a permanent loss — which is probably why this surfaced as a framework bug for three years.
There is no clean app-level workaround. The inline script cannot re-run — React deliberately creates every <script> via innerHTML "so its 'parser-inserted' flag is set to true and it does not execute" — so the attribute has to be re-applied from a useLayoutEffect on every navigation. useEffect is not enough: passive effects run after paint, so the wrong theme is visible for a frame. That is what I ended up shipping, but it is a per-app patch for something the singleton contract seems like it should guarantee.
packages/react-dom/src/__tests__/ReactDOMSingletonComponents-test.js has // @gate TODO / it('preserves imperative attributes when acquiring a singleton', …), covering the cold acquire. The remount entry into the same function is not covered. Happy to open a PR adding it as an additional gated test.
Disclosure: I used an AI assistant to investigate this and to build the reproductions. I hit the bug in my own app, and I have run both reproductions myself.
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.