diff --git a/.changeset/orange-lobsters-lay.md b/.changeset/orange-lobsters-lay.md
new file mode 100644
index 0000000000..0e66f00606
--- /dev/null
+++ b/.changeset/orange-lobsters-lay.md
@@ -0,0 +1,5 @@
+---
+"@react-router/dev": patch
+---
+
+fix(dev): add param to route css so it is not deduped by react
diff --git a/.github/workflows/close-no-repro-issues.yml b/.github/workflows/close-no-repro-issues.yml
index f9a6e2e45f..efc77ff396 100644
--- a/.github/workflows/close-no-repro-issues.yml
+++ b/.github/workflows/close-no-repro-issues.yml
@@ -31,7 +31,7 @@ jobs:
uses: pnpm/action-setup@v4.1.0
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
# required for --experimental-strip-types
node-version: 22
diff --git a/.github/workflows/deduplicate-lock-file.yml b/.github/workflows/deduplicate-lock-file.yml
index 51d6c758b4..3174fcfcbb 100644
--- a/.github/workflows/deduplicate-lock-file.yml
+++ b/.github/workflows/deduplicate-lock-file.yml
@@ -26,7 +26,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 86ced6e59e..1e6391dfd8 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -35,7 +35,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: pnpm
diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml
index 459c848d12..ff63cd21ee 100644
--- a/.github/workflows/format.yml
+++ b/.github/workflows/format.yml
@@ -25,7 +25,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: pnpm
diff --git a/.github/workflows/release-experimental.yml b/.github/workflows/release-experimental.yml
index 1b4624d7f4..373afa10ba 100644
--- a/.github/workflows/release-experimental.yml
+++ b/.github/workflows/release-experimental.yml
@@ -29,7 +29,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/release-nightly.yml b/.github/workflows/release-nightly.yml
index b394521418..be5ec510e8 100644
--- a/.github/workflows/release-nightly.yml
+++ b/.github/workflows/release-nightly.yml
@@ -40,7 +40,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/release-stage-2-alpha.yml b/.github/workflows/release-stage-2-alpha.yml
index 40157097f3..c82f906205 100644
--- a/.github/workflows/release-stage-2-alpha.yml
+++ b/.github/workflows/release-stage-2-alpha.yml
@@ -35,7 +35,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 366c8dcc9f..b88bb74292 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -35,7 +35,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
@@ -84,7 +84,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml
index 345ec9c300..4a3617aa6c 100644
--- a/.github/workflows/shared-build.yml
+++ b/.github/workflows/shared-build.yml
@@ -17,7 +17,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version-file: ".nvmrc"
cache: "pnpm"
diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml
index 0454fc2cca..782507c3ac 100644
--- a/.github/workflows/shared-integration.yml
+++ b/.github/workflows/shared-integration.yml
@@ -44,7 +44,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node ${{ matrix.node }}
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: "pnpm"
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d510f93a25..2bf0dedbe9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -39,7 +39,7 @@ jobs:
uses: pnpm/action-setup@v4
- name: ⎔ Setup node
- uses: actions/setup-node@v5
+ uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: pnpm
diff --git a/contributors.yml b/contributors.yml
index 0b1dee6722..7f5bbd347a 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -190,6 +190,7 @@
- johnpangalos
- jonkoops
- joseph0926
+- joshuaellis
- jplhomer
- jrakotoharisoa
- jrestall
@@ -308,6 +309,7 @@
- pawelblaszczyk5
- pcattori
- penx
+- peterneave
- petersendidit
- phildl
- phryneas
diff --git a/docs/api/data-routers/createHashRouter.md b/docs/api/data-routers/createHashRouter.md
index 528410ec37..307944a3fa 100644
--- a/docs/api/data-routers/createHashRouter.md
+++ b/docs/api/data-routers/createHashRouter.md
@@ -23,7 +23,7 @@ https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/do
[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.createHashRouter.html)
Create a new [data router](https://api.reactrouter.com/v7/interfaces/react_router.DataRouter.html) that manages the application
-path via the URL [`hash`]https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).
+path via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).
## Signature
diff --git a/integration/css-lazy-loading-test.ts b/integration/css-lazy-loading-test.ts
new file mode 100644
index 0000000000..852ecdc857
--- /dev/null
+++ b/integration/css-lazy-loading-test.ts
@@ -0,0 +1,183 @@
+import { test, expect } from "@playwright/test";
+
+import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
+import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
+import {
+ createAppFixture,
+ createFixture,
+ css,
+ js,
+} from "./helpers/create-fixture.js";
+
+let fixture: Fixture;
+let appFixture: AppFixture;
+
+test.beforeEach(async ({ context }) => {
+ await context.route(/\.data$/, async (route) => {
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ route.continue();
+ });
+});
+
+test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {
+ "app/routes.ts": js`
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
+
+ export default [
+ index("routes/home.tsx"),
+ route("company", "routes/layout.tsx", [
+ route("books", "routes/books/route.tsx"),
+ route("publishers", "routes/publishers/route.tsx"),
+ ]),
+ ] satisfies RouteConfig;
+ `,
+
+ "app/components/Icon.module.css": css`
+ .icon {
+ width: 20px;
+ height: 20px;
+ background-color: green;
+ }
+ `,
+
+ "app/components/Icon.tsx": js`
+ import styles from "./Icon.module.css";
+
+ export const Icon = () => {
+ return
;
+ }
+ `,
+
+ "app/components/LazyIcon.tsx": js`
+ import { lazy, Suspense } from "react";
+
+ const Icon = lazy(() =>
+ import("../components/Icon").then((m) => ({ default: m.Icon }))
+ );
+
+ const LazyIcon = ({ show }: { show: boolean }) => {
+ if (!show) return null;
+
+ return (
+ Loading...}>
+
+
+ );
+ };
+
+ export { LazyIcon };
+ `,
+
+ "app/routes/home.tsx": js`
+ import { redirect } from "react-router";
+
+ export const loader = () => {
+ return redirect("/company/books");
+ };
+ `,
+
+ "app/routes/layout.tsx": js`
+ import { Link, Outlet } from "react-router";
+
+ import { LazyIcon } from "../components/LazyIcon";
+ import { useState, useEffect } from "react";
+
+ export default function Layout() {
+ const [hydrated, setHydrated] = useState(false);
+ const [show, setShow] = useState(false);
+
+ useEffect(() => {
+ setShow(true);
+ },[])
+
+ return (
+
+
Layout
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+
+ "app/routes/books/route.tsx": js`
+ import { Icon } from "../../components/Icon";
+
+ export default function BooksRoute() {
+ return (
+ <>
+ Books
+
+
+
+ >
+ );
+ }
+
+ `,
+
+ "app/routes/publishers/route.tsx": js`
+ export default function PublishersRoute() {
+ return Publishers
;
+ }
+ `,
+ },
+ });
+
+ // This creates an interactive app using playwright.
+ appFixture = await createAppFixture(fixture);
+});
+
+test.afterAll(() => {
+ appFixture.close();
+});
+
+test("should preserve the CSS from the lazy loaded component even when it's in the route css manifest", async ({
+ page,
+}) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/");
+
+ expect(await page.getByTestId("icon").all()).toHaveLength(1);
+
+ // check the head for a link to the css that includes the word `Icon`
+ const links1 = await page.$$("link");
+ let found1 = false;
+ for (const link of links1) {
+ const href = await link.getAttribute("href");
+ if (href?.includes("Icon") && href.includes("css")) {
+ found1 = true;
+ }
+ }
+
+ expect(found1).toBe(true);
+
+ // wait for the loading to be gone before checking the lazy loaded component has resolved
+ await expect(page.getByText("Loading...")).toHaveCount(0);
+ expect(await page.getByTestId("icon").all()).toHaveLength(2);
+
+ await app.clickLink("/company/publishers");
+
+ expect(await page.getByTestId("icon").all()).toHaveLength(1);
+
+ const links2 = await page.$$("link");
+ let found2 = false;
+ for (const link of links2) {
+ const href = await link.getAttribute("href");
+ if (href?.includes("Icon") && href.includes("css")) {
+ found2 = true;
+ }
+ }
+
+ expect(found2).toBe(true);
+});
diff --git a/integration/vite-dot-server-test.ts b/integration/vite-dot-server-test.ts
index 77e749d192..6e686d6b01 100644
--- a/integration/vite-dot-server-test.ts
+++ b/integration/vite-dot-server-test.ts
@@ -146,7 +146,7 @@ test.describe("Vite / route / server-only module referenced by client", () => {
` But other route exports in 'app/routes/_index.tsx' depend on '${specifier}'.`,
- " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code",
+ " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code",
].forEach(expect(stderr).toMatch);
});
}
@@ -206,7 +206,7 @@ test.describe("Vite / non-route / server-only module referenced by client", () =
` '${specifier}' imported by 'app/reexport-server-only.ts'`,
- " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code",
+ " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code",
].forEach(expect(stderr).toMatch);
});
}
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index d409f808c0..a4afbd0260 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -394,7 +394,7 @@ const getReactRouterManifestBuildAssets = (
: null,
chunks
.flatMap((e) => e.css ?? [])
- .map((href) => `${ctx.publicPath}${href}`),
+ .map((href) => `${ctx.publicPath}${href}#route=true`),
]
.flat(1)
.filter(isNonNullable),
@@ -2084,7 +2084,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
"",
` But other route exports in '${importerShort}' depend on '${id}'.`,
"",
- " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code",
+ " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code",
"",
].join("\n"),
);
@@ -2096,7 +2096,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
"",
` '${id}' imported by '${importerShort}'`,
"",
- " See https://remix.run/docs/en/main/guides/vite#splitting-up-client-and-server-code",
+ " See https://reactrouter.com/explanation/code-splitting#removal-of-server-code",
"",
].join("\n"),
);
diff --git a/packages/react-router-fs-routes/index.ts b/packages/react-router-fs-routes/index.ts
index a68afc85e2..11d02ffb89 100644
--- a/packages/react-router-fs-routes/index.ts
+++ b/packages/react-router-fs-routes/index.ts
@@ -12,7 +12,7 @@ import { normalizeSlashes } from "./normalizeSlashes";
/**
* Creates route config from the file system using a convention that matches
* [Remix v2's route file
- * naming](https://remix.run/docs/en/v2/file-conventions/routes-files), for use
+ * naming](https://v2.remix.run/docs/file-conventions/routes), for use
* within `routes.ts`.
*/
export async function flatRoutes(
diff --git a/packages/react-router-node/sessions/fileStorage.ts b/packages/react-router-node/sessions/fileStorage.ts
index 3d2c30ae41..840d463a5f 100644
--- a/packages/react-router-node/sessions/fileStorage.ts
+++ b/packages/react-router-node/sessions/fileStorage.ts
@@ -26,7 +26,7 @@ interface FileSessionStorageOptions {
* The advantage of using this instead of cookie session storage is that
* files may contain much more data than cookies.
*
- * @see https://remix.run/utils/sessions#createfilesessionstorage-node
+ * @see https://api.reactrouter.com/v7/functions/_react_router_node.createFileSessionStorage
*/
export function createFileSessionStorage({
cookie,
diff --git a/packages/react-router-remix-routes-option-adapter/index.ts b/packages/react-router-remix-routes-option-adapter/index.ts
index 77f2582068..219b7a5780 100644
--- a/packages/react-router-remix-routes-option-adapter/index.ts
+++ b/packages/react-router-remix-routes-option-adapter/index.ts
@@ -11,7 +11,7 @@ export type { DefineRoutesFunction, DefineRouteFunction };
/**
* Adapts routes defined using [Remix's `routes` config
- * option](https://remix.run/docs/en/v2/file-conventions/vite-config#routes) to
+ * option](https://v2.remix.run/docs/file-conventions/vite-config#routes) to
* React Router's config format, for use within `routes.ts`.
*/
export async function remixRoutesOptionAdapter(
diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx
index ecbe52374e..92cda6b050 100644
--- a/packages/react-router/lib/dom/lib.tsx
+++ b/packages/react-router/lib/dom/lib.tsx
@@ -766,7 +766,7 @@ export function createBrowserRouter(
/**
* Create a new {@link DataRouter| data router} that manages the application
- * path via the URL [`hash`]https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).
+ * path via the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash).
*
* @public
* @category Data Routers
diff --git a/packages/react-router/lib/dom/ssr/routeModules.ts b/packages/react-router/lib/dom/ssr/routeModules.ts
index c06e8b617c..e8926d46b4 100644
--- a/packages/react-router/lib/dom/ssr/routeModules.ts
+++ b/packages/react-router/lib/dom/ssr/routeModules.ts
@@ -119,7 +119,7 @@ export type LayoutComponent = ComponentType<{
* A function that defines `` tags to be inserted into the `` of
* the document on route transitions.
*
- * @see https://remix.run/route/meta
+ * @see https://reactrouter.com/start/framework/route-module#meta
*/
export interface LinksFunction {
(): LinkDescriptor[];
@@ -267,7 +267,7 @@ export type RouteComponent = ComponentType<{}>;
/**
* An arbitrary object that is associated with a route.
*
- * @see https://remix.run/route/handle
+ * @see https://reactrouter.com/how-to/using-handle
*/
export type RouteHandle = unknown;