Skip to content

Commit 1013aae

Browse files
committed
poc
1 parent 02339a1 commit 1013aae

File tree

6 files changed

+157
-9
lines changed

6 files changed

+157
-9
lines changed

packages/react-router/lib/components.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,9 @@ export function RouterProvider({
521521
flushSync: reactDomFlushSyncImpl,
522522
unstable_onError,
523523
}: RouterProviderProps): React.ReactElement {
524-
let [state, setStateImpl] = React.useState(router.state);
524+
let [_state, setStateImpl] = React.useState(router.state);
525+
// @ts-expect-error - Needs React 19 types
526+
let [state, setOptimisticState] = React.useOptimistic(_state);
525527
let [pendingState, setPendingState] = React.useState<RouterState>();
526528
let [vtContext, setVtContext] = React.useState<ViewTransitionContextObject>({
527529
isTransitioning: false,
@@ -591,7 +593,10 @@ export function RouterProvider({
591593
if (reactDomFlushSyncImpl && flushSync) {
592594
reactDomFlushSyncImpl(() => logErrorsAndSetState(newState));
593595
} else {
594-
React.startTransition(() => logErrorsAndSetState(newState));
596+
React.startTransition(() => {
597+
setOptimisticState(newState);
598+
logErrorsAndSetState(newState);
599+
});
595600
}
596601
return;
597602
}

packages/react-router/lib/dom/lib.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2539,7 +2539,7 @@ export function useSubmit(): SubmitFunction {
25392539
let { basename } = React.useContext(NavigationContext);
25402540
let currentRouteId = useRouteId();
25412541

2542-
return React.useCallback<SubmitFunction>(
2542+
const submit = React.useCallback<SubmitFunction>(
25432543
async (target, options = {}) => {
25442544
let { action, method, encType, formData, body } = getFormSubmissionInfo(
25452545
target,
@@ -2573,6 +2573,49 @@ export function useSubmit(): SubmitFunction {
25732573
},
25742574
[router, basename, currentRouteId],
25752575
);
2576+
2577+
return React.useCallback<SubmitFunction>(
2578+
(target, options) => {
2579+
const deferred = new Deferred<void>();
2580+
// @ts-expect-error - Needs React 19 types
2581+
React.startTransition(async () => {
2582+
try {
2583+
await submit(target, options);
2584+
deferred.resolve();
2585+
} catch (error) {
2586+
deferred.reject(error);
2587+
}
2588+
});
2589+
return deferred.promise;
2590+
},
2591+
[submit],
2592+
);
2593+
}
2594+
2595+
// TODO: Move to a shared location
2596+
class Deferred<T> {
2597+
status: "pending" | "resolved" | "rejected" = "pending";
2598+
promise: Promise<T>;
2599+
// @ts-expect-error - no initializer
2600+
resolve: (value: T) => void;
2601+
// @ts-expect-error - no initializer
2602+
reject: (reason?: unknown) => void;
2603+
constructor() {
2604+
this.promise = new Promise((resolve, reject) => {
2605+
this.resolve = (value) => {
2606+
if (this.status === "pending") {
2607+
this.status = "resolved";
2608+
resolve(value);
2609+
}
2610+
};
2611+
this.reject = (reason) => {
2612+
if (this.status === "pending") {
2613+
this.status = "rejected";
2614+
reject(reason);
2615+
}
2616+
};
2617+
});
2618+
}
25762619
}
25772620

25782621
// v7: Eventually we should deprecate this entirely in favor of using the

packages/react-router/lib/hooks.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1825,15 +1825,58 @@ function useNavigateStable(): NavigateFunction {
18251825
if (!activeRef.current) return;
18261826

18271827
if (typeof to === "number") {
1828-
router.navigate(to);
1828+
await router.navigate(to);
18291829
} else {
18301830
await router.navigate(to, { fromRouteId: id, ...options });
18311831
}
18321832
},
18331833
[router, id],
18341834
);
18351835

1836-
return navigate;
1836+
let navigateTransition = React.useCallback(
1837+
(to: To | number, options: NavigateOptions = {}) => {
1838+
const deferred = new Deferred<void>();
1839+
// @ts-expect-error - Needs React 19 types
1840+
React.startTransition(async () => {
1841+
try {
1842+
await navigate(to as To, options);
1843+
deferred.resolve();
1844+
} catch (e) {
1845+
deferred.reject(e);
1846+
}
1847+
});
1848+
return deferred.promise;
1849+
},
1850+
[navigate],
1851+
);
1852+
1853+
return navigateTransition;
1854+
}
1855+
1856+
// TODO: Move to a shared location
1857+
class Deferred<T> {
1858+
status: "pending" | "resolved" | "rejected" = "pending";
1859+
promise: Promise<T>;
1860+
// @ts-expect-error - no initializer
1861+
resolve: (value: T) => void;
1862+
// @ts-expect-error - no initializer
1863+
reject: (reason?: unknown) => void;
1864+
constructor() {
1865+
this.promise = new Promise((resolve, reject) => {
1866+
this.resolve = (value) => {
1867+
if (this.status === "pending") {
1868+
this.status = "resolved";
1869+
resolve(value);
1870+
}
1871+
};
1872+
this.reject = (reason) => {
1873+
if (this.status === "pending") {
1874+
this.status = "rejected";
1875+
reject(reason);
1876+
}
1877+
};
1878+
});
1879+
}
18371880
}
18381881

18391882
const alreadyWarned: Record<string, boolean> = {};

playground/framework/app/root.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import { useTransition } from "react";
12
import {
3+
useNavigate,
24
Link,
35
Links,
46
Meta,
57
Outlet,
68
Scripts,
79
ScrollRestoration,
10+
useNavigation,
811
} from "react-router";
912

1013
export function Layout({ children }: { children: React.ReactNode }) {
14+
const [pending, startTransition] = useTransition();
15+
const navigate = useNavigate();
16+
const navigation = useNavigation();
17+
1118
return (
1219
<html lang="en">
1320
<head>
@@ -29,7 +36,31 @@ export function Layout({ children }: { children: React.ReactNode }) {
2936
Product
3037
</Link>
3138
</li>
39+
<li>
40+
<button
41+
onClick={() => {
42+
// @ts-expect-error - Needs React 19 types
43+
startTransition(() => navigate("/"));
44+
}}
45+
>
46+
Home
47+
</button>
48+
</li>
49+
<li>
50+
<button
51+
onClick={() => {
52+
// @ts-expect-error - Needs React 19 types
53+
startTransition(() => navigate("/products/abc"));
54+
}}
55+
>
56+
Product
57+
</button>
58+
</li>
59+
<li>{pending ? "Loading..." : "Idle"}</li>
3260
</ul>
61+
<pre>
62+
<p>{JSON.stringify(navigation)}</p>
63+
</pre>
3364
{children}
3465
<ScrollRestoration />
3566
<Scripts />

playground/framework/app/routes/_index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Route } from "./+types/_index";
22

3-
export function loader({ params }: Route.LoaderArgs) {
3+
export async function loader({ params }: Route.LoaderArgs) {
4+
await new Promise((resolve) => setTimeout(resolve, 1000));
45
return { planet: "world", date: new Date(), fn: () => 1 };
56
}
67

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1+
import { Form, useNavigation } from "react-router";
12
import type { Route } from "./+types/product";
3+
import { useTransition } from "react";
24

3-
export function loader({ params }: Route.LoaderArgs) {
5+
export async function loader({ params }: Route.LoaderArgs) {
6+
await new Promise((resolve) => setTimeout(resolve, 1000));
47
return { name: `Super cool product #${params.id}` };
58
}
69

7-
export default function Component({ loaderData }: Route.ComponentProps) {
8-
return <h1>{loaderData.name}</h1>;
10+
export async function action() {
11+
await new Promise((resolve) => setTimeout(resolve, 1000));
12+
return "Action complete!";
13+
}
14+
15+
export default function Component({
16+
actionData,
17+
loaderData,
18+
}: Route.ComponentProps) {
19+
const [pending, setPending] = useTransition();
20+
return (
21+
<>
22+
<h1>{loaderData.name}</h1>
23+
<p>{pending ? "Loading..." : "Idle"}</p>
24+
<Form
25+
onSubmit={() => {
26+
setPending(() => {});
27+
}}
28+
>
29+
<button type="submit">Perform Action</button>
30+
</Form>
31+
{actionData && <p>{actionData}</p>}
32+
</>
33+
);
934
}

0 commit comments

Comments
 (0)