Skip to content

Commit 0f5be98

Browse files
committed
Add transitions playground
1 parent 41ad237 commit 0f5be98

File tree

15 files changed

+408
-7
lines changed

15 files changed

+408
-7
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules
2+
3+
/build
4+
.env
5+
6+
.react-router/
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { startTransition, StrictMode } from "react";
2+
import { hydrateRoot } from "react-dom/client";
3+
import { HydratedRouter } from "react-router/dom";
4+
5+
let searchParams = new URLSearchParams(window.location.search);
6+
let transitions = searchParams.has("transitions")
7+
? searchParams.get("transitions") === "true"
8+
: undefined;
9+
10+
startTransition(() => {
11+
hydrateRoot(
12+
document,
13+
<StrictMode>
14+
<HydratedRouter unstable_transitions={transitions} />
15+
</StrictMode>,
16+
);
17+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {
2+
Link,
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
} from "react-router";
9+
10+
export function Layout({ children }: { children: React.ReactNode }) {
11+
return (
12+
<html lang="en">
13+
<head>
14+
<meta charSet="utf-8" />
15+
<meta name="viewport" content="width=device-width, initial-scale=1" />
16+
17+
<Meta />
18+
<Links />
19+
</head>
20+
<body>
21+
{children}
22+
<ScrollRestoration />
23+
<Scripts />
24+
</body>
25+
</html>
26+
);
27+
}
28+
29+
export default function App() {
30+
return <Outlet />;
31+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { type RouteConfig } from "@react-router/dev/routes";
2+
import { flatRoutes } from "@react-router/fs-routes";
3+
4+
export default flatRoutes() satisfies RouteConfig;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { redirect } from "react-router";
2+
import type { Route } from "./+types/_index";
3+
4+
export function loader({ request }: Route.LoaderArgs) {
5+
return redirect("/transitions" + new URL(request.url).search);
6+
}
7+
8+
export default function Index() {
9+
return null;
10+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as React from "react";
2+
import { Await, Outlet } from "react-router";
3+
4+
import type { Route } from "./+types/transitions.parent.child";
5+
6+
export async function loader() {
7+
await new Promise((r) => setTimeout(r, 1000));
8+
let promise = new Promise((r) => setTimeout(() => r("CHILD DATA"), 2000));
9+
return { promise };
10+
}
11+
12+
export default function Transitions({ loaderData }: Route.ComponentProps) {
13+
return (
14+
<>
15+
<h3>Child</h3>
16+
<Await resolve={loaderData.promise}>
17+
{(data) => <p>Data: {data}</p>}
18+
</Await>
19+
</>
20+
);
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as React from "react";
2+
import { Await, Outlet } from "react-router";
3+
4+
import type { Route } from "./+types/transitions.parent";
5+
6+
export async function loader() {
7+
await new Promise((r) => setTimeout(r, 1000));
8+
let promise = new Promise((r) => setTimeout(() => r("PARENT DATA"), 1000));
9+
return { promise };
10+
}
11+
12+
export default function Transitions({ loaderData }: Route.ComponentProps) {
13+
return (
14+
<>
15+
<h2>Parent</h2>
16+
<React.Suspense fallback={<p>Loading parent data...</p>}>
17+
<Await resolve={loaderData.promise}>
18+
{(data) => <p>Data: {data}</p>}
19+
</Await>
20+
<Outlet />
21+
</React.Suspense>
22+
</>
23+
);
24+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export async function clientLoader() {
2+
await new Promise((r) => setTimeout(() => r("PARENT DATA"), 3000));
3+
return null;
4+
}
5+
6+
export default function Transitions() {
7+
return <h2>Slow Page</h2>;
8+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import * as React from "react";
2+
import { Link, Outlet, useNavigate, useNavigation } from "react-router";
3+
4+
export default function Transitions() {
5+
let navigate = useNavigate();
6+
let navigation = useNavigation();
7+
let [pending, startTransition] = React.useTransition();
8+
let [count, setCount] = React.useState(0);
9+
let [count2, setCount2] = React.useState(0);
10+
return (
11+
<>
12+
<h1>Transitions</h1>
13+
<nav>
14+
Start Over:{" "}
15+
<a href="/transitions">
16+
<code>unstable_transitions=undefined</code>
17+
</a>
18+
{" | "}
19+
<a href="/transitions?transitions=true">
20+
<code>unstable_transitions=true</code>
21+
</a>
22+
{" | "}
23+
<a href="/transitions?transitions=false">
24+
<code>unstable_transitions=false</code>
25+
</a>
26+
</nav>
27+
<ul style={{ maxWidth: "600px" }}>
28+
<li>
29+
<button onClick={() => navigate("/transitions/slow")}>
30+
Slow navigation with <code>navigate</code>
31+
</button>
32+
<ul>
33+
<li>
34+
In the current state, <code>useNavigate</code> navigations are not
35+
wrapped in <code>startTransition</code>, so they don't play nice
36+
with other transition-aware state updates
37+
</li>
38+
<li>
39+
Fixed by{" "}
40+
<code>
41+
&lt;HydrateRouter unstable_transitions={"{"}true{"}"} /&gt;
42+
</code>
43+
</li>
44+
</ul>
45+
</li>
46+
47+
<li>
48+
<button
49+
onClick={() => navigate("/transitions/slow", { flushSync: true })}
50+
>
51+
Slow navigation with <code>navigate + flushSync</code>
52+
</button>
53+
<ul>
54+
<li>
55+
With the new flag, useNavigate automatically wraps the navigation
56+
in <code>React.startTransition</code>. Passing the{" "}
57+
<code>flushSync</code> option will opt out of that and apply
58+
<code>React.flushSync</code> to the underlying state update
59+
</li>
60+
</ul>
61+
</li>
62+
63+
<li>
64+
<button
65+
onClick={() => startTransition(() => navigate("/transitions/slow"))}
66+
>
67+
Slow navigation with local <code>startTransition + navigate</code>
68+
</button>
69+
<ul>
70+
<li>
71+
Once you wrap them in <code>startTransition</code>, they play
72+
nicely with those updates but they prevent our internal
73+
mid-navigation state updates from surfacing
74+
</li>
75+
<li>
76+
Fixed by{" "}
77+
<code>
78+
&lt;HydrateRouter unstable_transitions={"{"}true{"}"} /&gt;
79+
</code>
80+
</li>
81+
</ul>
82+
</li>
83+
84+
<li>
85+
<Link to="/transitions/slow">Slow Navigation via &lt;Link&gt;</Link>
86+
<ul>
87+
<li>
88+
In the current state, <code>&lt;Link&gt;</code> navigations are
89+
not wrapped in startTransition, so they don't play nice with other
90+
transition-aware state updates
91+
</li>
92+
<li>
93+
Fixed by{" "}
94+
<code>
95+
&lt;HydrateRouter unstable_transitions={"{"}true{"}"} /&gt;
96+
</code>
97+
</li>
98+
</ul>
99+
</li>
100+
101+
<li>
102+
<Link to="/transitions/parent">/transitions/parent</Link>
103+
</li>
104+
<li>
105+
<Link to="/transitions/parent/child">/transitions/parent/child</Link>
106+
</li>
107+
</ul>
108+
<div>
109+
<p
110+
style={{
111+
color: pending ? "green" : "black",
112+
fontWeight: pending ? "bold" : "normal",
113+
}}
114+
>
115+
React Transition: {pending ? "Pending" : "Idle"}
116+
</p>
117+
<p
118+
style={{
119+
color: navigation.state !== "idle" ? "green" : "black",
120+
fontWeight: navigation.state !== "idle" ? "bold" : "normal",
121+
}}
122+
>
123+
React Router Navigation State: {navigation.state}
124+
</p>
125+
</div>
126+
<button onClick={() => setCount((c) => c + 1)}>
127+
Increment counter w/o transition {count}
128+
</button>{" "}
129+
<button
130+
onClick={() => React.startTransition(() => setCount2((c) => c + 1))}
131+
>
132+
Increment counter w/transition {count2}
133+
</button>
134+
<Outlet />
135+
<p>
136+
TODO: Is it possible to demonstrate the issue with
137+
<code>React.useSyncExternalStore</code> where the global opt-out is
138+
needed?
139+
</p>
140+
</>
141+
);
142+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@playground/react-transitions",
3+
"version": "0.0.0",
4+
"private": true,
5+
"sideEffects": false,
6+
"type": "module",
7+
"scripts": {
8+
"build": "react-router build",
9+
"dev": "react-router dev",
10+
"start": "react-router-serve ./build/server/index.js",
11+
"typecheck": "react-router typegen && tsc"
12+
},
13+
"dependencies": {
14+
"@react-router/node": "workspace:*",
15+
"@react-router/serve": "workspace:*",
16+
"isbot": "^5.1.11",
17+
"react": "^19.1.0",
18+
"react-dom": "^19.1.0",
19+
"react-router": "workspace:*"
20+
},
21+
"devDependencies": {
22+
"@react-router/dev": "workspace:*",
23+
"@react-router/fs-routes": "^7.9.5",
24+
"@types/react": "^18.2.20",
25+
"@types/react-dom": "^18.2.7",
26+
"typescript": "^5.1.6",
27+
"vite": "^6.1.0",
28+
"vite-tsconfig-paths": "^4.2.1"
29+
},
30+
"engines": {
31+
"node": ">=20.0.0"
32+
}
33+
}

0 commit comments

Comments
 (0)