Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-hotels-complain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

[REMOVE] (continuation of #13872) In Framework Mode, remove leftover critical CSS elements in development after initial render if there's a hydration mismatch
17 changes: 17 additions & 0 deletions packages/react-router/lib/dom-export/hydrated-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
UNSAFE_hydrationRouteProperties as hydrationRouteProperties,
UNSAFE_createClientRoutesWithHMRRevalidationOptOut as createClientRoutesWithHMRRevalidationOptOut,
} from "react-router";
import { CRITICAL_CSS_DATA_ATTRIBUTE } from "../dom/ssr/components";
import { RouterProvider } from "./dom-router-provider";

type SSRInfo = {
Expand Down Expand Up @@ -242,6 +243,22 @@ export function HydratedRouter(props: HydratedRouterProps) {
setCriticalCss(undefined);
}
}, []);
React.useEffect(() => {
if (process.env.NODE_ENV === "development" && criticalCss === undefined) {
// When there's a hydration mismatch, React 19 ignores the server HTML and
// re-renders from the root, but it doesn't remove any head tags that
// aren't present in the virtual DOM. This means that the original
// critical CSS elements are still in the document even though we cleared
// them in the effect above. To fix this, this effect is designed to clean
// up any leftover elements. If `criticalCss` is undefined in this effect,
// this means that React is no longer managing the critical CSS elements,
// so if there are any left in the document, these are stale elements from
// the original SSR pass and we can safely remove them.
document
.querySelectorAll(`[${CRITICAL_CSS_DATA_ATTRIBUTE}]`)
.forEach((element) => element.remove());
}
}, [criticalCss]);

let [location, setLocation] = React.useState(router.state.location);

Expand Down
13 changes: 11 additions & 2 deletions packages/react-router/lib/dom/ssr/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ function getActiveMatches(
return matches;
}

export const CRITICAL_CSS_DATA_ATTRIBUTE = "data-react-router-critical-css";

/**
Renders all of the `<link>` tags created by route module {@link LinksFunction} export. You should render it inside the `<head>` of your document.

Expand Down Expand Up @@ -247,10 +249,17 @@ export function Links() {
return (
<>
{typeof criticalCss === "string" ? (
<style dangerouslySetInnerHTML={{ __html: criticalCss }} />
<style
{...{ [CRITICAL_CSS_DATA_ATTRIBUTE]: "" }}
dangerouslySetInnerHTML={{ __html: criticalCss }}
/>
) : null}
{typeof criticalCss === "object" ? (
<link rel="stylesheet" href={criticalCss.href} />
<link
{...{ [CRITICAL_CSS_DATA_ATTRIBUTE]: "" }}
rel="stylesheet"
href={criticalCss.href}
/>
) : null}
{keyedLinks.map(({ key, link }) =>
isPageLinkDescriptor(link) ? (
Expand Down