Skip to content

Commit 31e26ac

Browse files
committed
fix(dev): add param to route css so it is not deduped by react
1 parent 1abe213 commit 31e26ac

File tree

4 files changed

+192
-1
lines changed

4 files changed

+192
-1
lines changed

.changeset/orange-lobsters-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-router/dev": patch
3+
---
4+
5+
fix(dev): add param to route css so it is not deduped by react

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@
190190
- johnpangalos
191191
- jonkoops
192192
- joseph0926
193+
- joshuaellis
193194
- jplhomer
194195
- jrakotoharisoa
195196
- jrestall
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
import { PlaywrightFixture } from "./helpers/playwright-fixture.js";
4+
import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
5+
import {
6+
createAppFixture,
7+
createFixture,
8+
css,
9+
js,
10+
} from "./helpers/create-fixture.js";
11+
12+
let fixture: Fixture;
13+
let appFixture: AppFixture;
14+
15+
test.beforeEach(async ({ context }) => {
16+
await context.route(/\.data$/, async (route) => {
17+
await new Promise((resolve) => setTimeout(resolve, 50));
18+
route.continue();
19+
});
20+
});
21+
22+
test.beforeAll(async () => {
23+
fixture = await createFixture({
24+
files: {
25+
"app/routes.ts": js`
26+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
27+
28+
export default [
29+
index("routes/home.tsx"),
30+
route("company", "routes/layout.tsx", [
31+
route("books", "routes/books/route.tsx"),
32+
route("publishers", "routes/publishers/route.tsx"),
33+
]),
34+
] satisfies RouteConfig;
35+
`,
36+
37+
"app/components/Icon.module.css": css`
38+
.icon {
39+
width: 20px;
40+
height: 20px;
41+
background-color: green;
42+
}
43+
`,
44+
45+
"app/components/Icon.tsx": js`
46+
import styles from "./Icon.module.css";
47+
48+
export const Icon = () => {
49+
return <div data-testid="icon" className={styles.icon} />;
50+
}
51+
`,
52+
53+
"app/components/LazyIcon.tsx": js`
54+
import { lazy, Suspense } from "react";
55+
56+
const Icon = lazy(() =>
57+
import("../components/Icon").then((m) => ({ default: m.Icon }))
58+
);
59+
60+
const LazyIcon = ({ show }: { show: boolean }) => {
61+
if (!show) return null;
62+
63+
return (
64+
<Suspense fallback={<div>Loading...</div>}>
65+
<Icon />
66+
</Suspense>
67+
);
68+
};
69+
70+
export { LazyIcon };
71+
`,
72+
73+
"app/routes/home.tsx": js`
74+
import { redirect } from "react-router";
75+
76+
export const loader = () => {
77+
return redirect("/company/books");
78+
};
79+
`,
80+
81+
"app/routes/layout.tsx": js`
82+
import { Link, Outlet } from "react-router";
83+
84+
import { LazyIcon } from "../components/LazyIcon";
85+
import { useState, useEffect } from "react";
86+
87+
export default function Layout() {
88+
const [hydrated, setHydrated] = useState(false);
89+
const [show, setShow] = useState(false);
90+
91+
useEffect(() => {
92+
setShow(true);
93+
},[])
94+
95+
return (
96+
<div style={{ border: "1px solid blue" }}>
97+
<h1>Layout</h1>
98+
<nav>
99+
<Link to="/company/books">Books</Link>
100+
<Link to="/company/publishers">Publishers</Link>
101+
</nav>
102+
<div>
103+
<LazyIcon show={show} />
104+
</div>
105+
<div style={{ border: "1px solid red" }}>
106+
<Outlet />
107+
</div>
108+
</div>
109+
);
110+
}
111+
`,
112+
113+
"app/routes/books/route.tsx": js`
114+
import { Icon } from "../../components/Icon";
115+
116+
export default function BooksRoute() {
117+
return (
118+
<>
119+
<h1>Books</h1>
120+
<div>
121+
<Icon />
122+
</div>
123+
</>
124+
);
125+
}
126+
127+
`,
128+
129+
"app/routes/publishers/route.tsx": js`
130+
export default function PublishersRoute() {
131+
return <h1>Publishers</h1>;
132+
}
133+
`,
134+
},
135+
});
136+
137+
// This creates an interactive app using playwright.
138+
appFixture = await createAppFixture(fixture);
139+
});
140+
141+
test.afterAll(() => {
142+
appFixture.close();
143+
});
144+
145+
test("should preserve the CSS from the lazy loaded component even when it's in the route css manifest", async ({
146+
page,
147+
}) => {
148+
let app = new PlaywrightFixture(appFixture, page);
149+
await app.goto("/");
150+
151+
expect(await page.getByTestId("icon").all()).toHaveLength(1);
152+
153+
// check the head for a link to the css that includes the word `Icon`
154+
const links1 = await page.$$("link");
155+
let found1 = false;
156+
for (const link of links1) {
157+
const href = await link.getAttribute("href");
158+
if (href?.includes("Icon") && href.includes("css")) {
159+
found1 = true;
160+
}
161+
}
162+
163+
expect(found1).toBe(true);
164+
165+
// wait for the loading to be gone before checking the lazy loaded component has resolved
166+
await expect(page.getByText("Loading...")).toHaveCount(0);
167+
expect(await page.getByTestId("icon").all()).toHaveLength(2);
168+
169+
await app.poke(60);
170+
171+
await app.clickLink("/company/publishers");
172+
173+
expect(await page.getByTestId("icon").all()).toHaveLength(1);
174+
175+
const links2 = await page.$$("link");
176+
let found2 = false;
177+
for (const link of links2) {
178+
const href = await link.getAttribute("href");
179+
if (href?.includes("Icon") && href.includes("css")) {
180+
found2 = true;
181+
}
182+
}
183+
184+
expect(found2).toBe(true);
185+
});

packages/react-router-dev/vite/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ const getReactRouterManifestBuildAssets = (
394394
: null,
395395
chunks
396396
.flatMap((e) => e.css ?? [])
397-
.map((href) => `${ctx.publicPath}${href}`),
397+
.map((href) => `${ctx.publicPath}${href}#route=true`),
398398
]
399399
.flat(1)
400400
.filter(isNonNullable),

0 commit comments

Comments
 (0)