Skip to content

Fix FOUC when navigating between hydrated client entries#11402

Merged
ryanflorence merged 2 commits into
mainfrom
fix/style-adoption-refcount
May 15, 2026
Merged

Fix FOUC when navigating between hydrated client entries#11402
ryanflorence merged 2 commits into
mainfrom
fix/style-adoption-refcount

Conversation

@brophdawg11
Copy link
Copy Markdown
Contributor

Summary

  • Adopt server styles by refcount instead of resetting the adopted stylesheet on every reload.
  • DOM preserved across a reload (e.g. the previous client entry's content inside a hydration boundary that diffNodes skips over) keeps its rules until the new module finishes loading and the virtual root re-renders, eliminating the flash of unstyled content.
  • replaceServerStyles(source) on the style manager handles the prior/new diff, releasing only adoption refs of selectors absent from the next page — selectors held by an active css mixin (e.g. transient client-only state) are untouched.

Root cause

frame.ts used to call styleManager.reset() then adoptServerStyles() before diffNodes(). The synchronous block is fine for ordinary DOM, but diffNodes deliberately skips over hydrated client-entry boundaries and lets the hydration pass re-render them — that is an async step (module load). Between the style swap and the hydration re-render, the lingering previous-entry DOM had class names referring to rules reset() had just dropped.

Test plan

  • pnpm --filter @remix-run/ui run test — 756 pass / 0 fail, including the new regression test in src/test/frame.test.tsx that gates module loading to assert the prior entry's rule stays live while its DOM is still visible
  • pnpm --filter @remix-run/ui run typecheck
  • oxlint clean on changed files
  • Prettier clean on changed files
  • Manual: in the docs site, navigate /api/remix/ui/button/demos/basic//api/remix/ui/button/demos/states/ and confirm the preview frame swaps without a visible flash

🤖 Generated with Claude Code

Adopt server styles by refcount instead of resetting the adopted
stylesheet on every reload. DOM preserved across a reload (e.g. inside a
still-hydrated client-entry boundary) keeps its rules until the new
module finishes loading and the virtual root re-renders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Preview Build Available

A preview build has been created for this PR. You can install it using:

pnpm install "remix-run/remix#preview/pr-11402&path:packages/remix"

This preview build will be updated automatically as you push new commits.

@ryanflorence ryanflorence merged commit a2f15bd into main May 15, 2026
26 checks passed
@ryanflorence ryanflorence deleted the fix/style-adoption-refcount branch May 15, 2026 19:19
@github-actions
Copy link
Copy Markdown
Contributor

The preview branch preview/pr-11402 has been deleted now that this PR is merged/closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants