When Remix applies an incoming document during a client-side navigation, any content positioned after a hydrated clientEntry(...) is silently discarded. The URL updates, the elements before that position render, and the rest never appears. No console warning, no error.
Repro: https://github.com/luisrudge/remix-nav-repro (remix@3.0.0-beta.5)
pnpm install && pnpm exec playwright install chromium
pnpm test
PASS control: #tail exists after /control -> /plain
PASS control: #tail exists after /control -> /divroot
FAIL island: expected #tail after /island -> /plain; found body children header, script
FAIL nested: expected #tail after /nested -> /divroot; found body children div, script, root children header
FAIL Back: expected #tail after /plain -> /island -> Back; found body children header, script
/island renders a trivial hydrated entry as the body's first child. Navigating to /plain, whose body is [header, div#tail], produces a page containing only the <header>.
Isolation — the island is identical in every row; only its position and wrapper tag change:
| From |
Entry sits at |
Destination root |
Result |
/island → /plain |
body index 0 |
<header> |
#tail dropped |
/plain → /island → Back |
body index 0 |
<header> |
#tail dropped |
/nested → /divroot |
inside <div> |
<div> |
#tail dropped one level deeper |
/control → /plain |
inside <main> |
<header> |
ok |
/control → /divroot |
inside <main> |
<div> |
ok |
It's not about hydration. The control island hydrates too — the test clicks its counter before every navigation — and it works fine. What matters is where the entry sits.
Wrapping the entry doesn't fix it either. /nested and /control are the same page with a different wrapper tag, going to the same place. /nested uses a <div>, the destination also starts with a <div>, so the diff goes inside both and drops content one level down instead. /control uses a <main>, nothing matches, and it works.
So the only thing that saves you is picking a wrapper tag no other page uses at its root. That's easy to get wrong, and it breaks again the moment another page starts using that tag.
I hit this in a real app. It looked like a blank page after clicking a link or hitting Back, and took a while to track down because nothing errors.
Environment: remix@3.0.0-beta.5, Node 24.18.0, macOS 15, Chromium via Playwright 1.62.
When Remix applies an incoming document during a client-side navigation, any content positioned after a hydrated
clientEntry(...)is silently discarded. The URL updates, the elements before that position render, and the rest never appears. No console warning, no error.Repro: https://github.com/luisrudge/remix-nav-repro (
remix@3.0.0-beta.5)/islandrenders a trivial hydrated entry as the body's first child. Navigating to/plain, whose body is[header, div#tail], produces a page containing only the<header>.Isolation — the island is identical in every row; only its position and wrapper tag change:
/island→/plainbodyindex 0<header>#taildropped/plain→/island→ Backbodyindex 0<header>#taildropped/nested→/divroot<div><div>#taildropped one level deeper/control→/plain<main><header>/control→/divroot<main><div>It's not about hydration. The control island hydrates too — the test clicks its counter before every navigation — and it works fine. What matters is where the entry sits.
Wrapping the entry doesn't fix it either.
/nestedand/controlare the same page with a different wrapper tag, going to the same place./nesteduses a<div>, the destination also starts with a<div>, so the diff goes inside both and drops content one level down instead./controluses a<main>, nothing matches, and it works.So the only thing that saves you is picking a wrapper tag no other page uses at its root. That's easy to get wrong, and it breaks again the moment another page starts using that tag.
I hit this in a real app. It looked like a blank page after clicking a link or hitting Back, and took a while to track down because nothing errors.
Environment:
remix@3.0.0-beta.5, Node 24.18.0, macOS 15, Chromium via Playwright 1.62.