Skip to content

Commit 37a4218

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

File tree

5 files changed

+328
-24
lines changed

5 files changed

+328
-24
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

integration/bug-report-test.ts

Lines changed: 136 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Fixture, AppFixture } from "./helpers/create-fixture.js";
55
import {
66
createAppFixture,
77
createFixture,
8+
css,
89
js,
910
} from "./helpers/create-fixture.js";
1011

@@ -64,27 +65,113 @@ test.beforeAll(async () => {
6465
// `createFixture` will make an app and run your tests against it.
6566
////////////////////////////////////////////////////////////////////////////
6667
files: {
67-
"app/routes/_index.tsx": js`
68-
import { useLoaderData, Link } from "react-router";
68+
"app/routes.ts": js`
69+
import { type RouteConfig, index, route } from "@react-router/dev/routes";
70+
71+
export default [
72+
index("routes/home.tsx"),
73+
route("company", "routes/layout.tsx", [
74+
route("books", "routes/books/route.tsx"),
75+
route("publishers", "routes/publishers/route.tsx"),
76+
]),
77+
] satisfies RouteConfig;
78+
`,
79+
80+
"app/components/Icon.module.css": css`
81+
.icon {
82+
width: 20px;
83+
height: 20px;
84+
background-color: green;
85+
}
86+
`,
87+
88+
"app/components/Icon.tsx": js`
89+
import styles from "./Icon.module.css";
6990
70-
export function loader() {
71-
return "pizza";
91+
export const Icon = () => {
92+
return <div data-testid="icon" className={styles.icon} />;
7293
}
94+
`,
95+
96+
"app/components/LazyIcon.tsx": js`
97+
import { lazy, Suspense } from "react";
98+
99+
const Icon = lazy(() =>
100+
import("../components/Icon").then((m) => ({ default: m.Icon }))
101+
);
102+
103+
const LazyIcon = ({ show }: { show: boolean }) => {
104+
if (!show) return null;
73105
74-
export default function Index() {
75-
let data = useLoaderData();
76106
return (
77-
<div>
78-
{data}
79-
<Link to="/burgers">Other Route</Link>
107+
<Suspense fallback={<div>Loading...</div>}>
108+
<Icon />
109+
</Suspense>
110+
);
111+
};
112+
113+
export { LazyIcon };
114+
`,
115+
116+
"app/routes/home.tsx": js`
117+
import { redirect } from "react-router";
118+
119+
export const loader = () => {
120+
return redirect("/company/books");
121+
};
122+
`,
123+
124+
"app/routes/layout.tsx": js`
125+
import { Link, Outlet } from "react-router";
126+
127+
import { LazyIcon } from "../components/LazyIcon";
128+
import { useState, useEffect } from "react";
129+
130+
export default function Layout() {
131+
const [hydrated, setHydrated] = useState(false);
132+
const [show, setShow] = useState(false);
133+
134+
useEffect(() => {
135+
setShow(true);
136+
},[])
137+
138+
return (
139+
<div style={{ border: "1px solid blue" }}>
140+
<h1>Layout</h1>
141+
<nav>
142+
<Link to="/company/books">Books</Link>
143+
<Link to="/company/publishers">Publishers</Link>
144+
</nav>
145+
<div>
146+
<LazyIcon show={show} />
147+
</div>
148+
<div style={{ border: "1px solid red" }}>
149+
<Outlet />
150+
</div>
80151
</div>
81-
)
152+
);
153+
}
154+
`,
155+
156+
"app/routes/books/route.tsx": js`
157+
import { Icon } from "../../components/Icon";
158+
159+
export default function BooksRoute() {
160+
return (
161+
<>
162+
<h1>Books</h1>
163+
<div>
164+
<Icon />
165+
</div>
166+
</>
167+
);
82168
}
169+
83170
`,
84171

85-
"app/routes/burgers.tsx": js`
86-
export default function Index() {
87-
return <div>cheeseburger</div>;
172+
"app/routes/publishers/route.tsx": js`
173+
export default function PublishersRoute() {
174+
return <h1>Publishers</h1>;
88175
}
89176
`,
90177
},
@@ -103,22 +190,48 @@ test.afterAll(() => {
103190
// add a good description for what you expect React Router to do 👇🏽
104191
////////////////////////////////////////////////////////////////////////////////
105192

106-
test("[description of what you expect it to do]", async ({ page }) => {
193+
test("should preserve the CSS from the lazy loaded component even when it's in the route css manifest", async ({
194+
page,
195+
}) => {
107196
let app = new PlaywrightFixture(appFixture, page);
108-
// You can test any request your app might get using `fixture`.
109-
let response = await fixture.requestDocument("/");
110-
expect(await response.text()).toMatch("pizza");
111197

112198
// If you need to test interactivity use the `app`
113199
await app.goto("/");
114-
await app.clickLink("/burgers");
115-
await page.waitForSelector("text=cheeseburger");
116200

117-
// If you're not sure what's going on, you can "poke" the app, it'll
118-
// automatically open up in your browser for 20 seconds, so be quick!
119-
// await app.poke(20);
201+
expect((await page.$$("data-testid=icon")).length).toBe(1);
202+
203+
// check the head for a link to the css that includes the word `Icon`
204+
const links1 = await page.$$("link");
205+
let found1 = false;
206+
for (const link of links1) {
207+
const href = await link.getAttribute("href");
208+
if (href?.includes("Icon") && href.includes("css")) {
209+
found1 = true;
210+
}
211+
}
212+
213+
expect(found1).toBe(true);
214+
215+
// wait for the loading to be gone
216+
await expect(page.getByText("Loading...")).toHaveCount(0);
217+
218+
// check there are two data-testid=icon elements
219+
expect(await page.getByTestId("icon").all()).toHaveLength(2);
220+
221+
await app.clickLink("/company/publishers");
222+
223+
expect(await page.getByTestId("icon").all()).toHaveLength(1);
224+
225+
const links2 = await page.$$("link");
226+
let found2 = false;
227+
for (const link of links2) {
228+
const href = await link.getAttribute("href");
229+
if (href?.includes("Icon") && href.includes("css")) {
230+
found2 = true;
231+
}
232+
}
120233

121-
// Go check out the other tests to see what else you can do.
234+
expect(found2).toBe(true);
122235
});
123236

124237
////////////////////////////////////////////////////////////////////////////////
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+
});

0 commit comments

Comments
 (0)