Skip to content

Commit c3406eb

Browse files
authored
feat: deferred API updates (#9095)
* fix: keep loaderData as promises and track values internally * chore: rename <Deferred> -> <Await> * chore: rename useDeferredData -> useAwaitedData * bundle bump * Fix up example of raw promise * clean up await error boundary * fix: cancel fetcher deferred loads on revalidations * Handle fetcher deferred rejections at error boundaries * change Await prop from value -> promise * Add changeset * Add test for not proying settled values through * Add tests for raw promises and <Await> * Handle raw values with Await * add useAsyncError, support raw objects, stop supporting arrays and single promises * deferred() -> defer() * change <Await promise> to <Await resolve> * bump bundle * Remove defer utility in favor of naked objects * Clean up test * Revert "Remove defer utility in favor of naked objects" This reverts commit 3fd7850. * disbale auto tracking on naked objects * useRouteError => useAsyncError in changesets * add rejection code path to raw Await example
1 parent 0bdd7ad commit c3406eb

File tree

21 files changed

+1083
-808
lines changed

21 files changed

+1083
-808
lines changed

.changeset/light-months-argue.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,46 @@
33
"@remix-run/router": patch
44
---
55

6-
Feat: adds `deferred` support to data routers
6+
Feat: adds `defer()` support to data routers
77

8-
Returning a `deferred` from a `loader` allows you to separate _critical_ loader data that you want to wait for prior to rendering the destination page from _non-critical_ data that you are OK to show a spinner for until it loads.
8+
Returning a `defer()` from a `loader` allows you to separate _critical_ loader data that you want to wait for prior to rendering the destination page from _non-critical_ data that you are OK to show a spinner for until it loads.
99

1010
```jsx
11-
// In your route loader, return a deferred() and choose per-key whether to
11+
// In your route loader, return a defer() and choose per-key whether to
1212
// await the promise or not. As soon as the awaited promises resolve, the
1313
// page will be rendered.
1414
function loader() {
15-
return deferred({
15+
return defer({
1616
critical: await getCriticalData(),
1717
lazy: getLazyData(),
1818
});
1919
};
2020

2121
// In your route element, grab the values from useLoaderData and render them
22-
// with <Deferred> inside a <Suspense> boundary
23-
function DeferredPage() {
22+
// with <Await> inside a <React.Suspense> boundary
23+
function Page() {
2424
let data = useLoaderData();
2525
return (
2626
<>
2727
<p>Critical Data: {data.critical}</p>
28-
<Suspense fallback={<p>Loading...</p>}>
29-
<Deferred value={data.lazy} errorElement={<RenderDeferredError />}>
30-
<RenderDeferredData />
31-
</Deferred>
32-
</Suspense>
28+
<React.Suspense fallback={<p>Loading...</p>}>
29+
<Await resolve={data.lazy} errorElement={<RenderError />}>
30+
<RenderData />
31+
</Await>
32+
</React.Suspense>
3333
</>
3434
);
3535
}
3636

3737
// Use separate components to render the data once it resolves, and access it
38-
// via the useDeferredData hook
39-
function RenderDeferredData() {
40-
let data = useDeferredData();
38+
// via the useAsyncValue hook
39+
function RenderData() {
40+
let data = useAsyncValue();
4141
return <p>Lazy: {data}</p>;
4242
}
4343

44-
function RenderDeferredError() {
45-
let data = useRouteError();
44+
function RenderError() {
45+
let data = useAsyncError();
4646
return <p>Error! {data.message} {data.stack}</p>;
4747
}
4848
```
@@ -51,16 +51,16 @@ If you want to skip the separate components, you can use the Render Props
5151
pattern and handle the rendering of the deferred data inline:
5252

5353
```jsx
54-
function DeferredPage() {
54+
function Page() {
5555
let data = useLoaderData();
5656
return (
5757
<>
5858
<p>Critical Data: {data.critical}</p>
59-
<Suspense fallback={<p>Loading...</p>}>
60-
<Deferred value={data.lazy} errorElement={<RenderDeferredError />}>
59+
<React.Suspense fallback={<p>Loading...</p>}>
60+
<Await resolve={data.lazy} errorElement={<RenderError />}>
6161
{(data) => <p>{data}</p>}
62-
</Deferred>
63-
</Suspense>
62+
</Await>
63+
</React.Suspense>
6464
</>
6565
);
6666
}

.changeset/new-kiwis-burn.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Deferred API Updates
88
- Removes `<Suspense>` from inside `<Deferred>`, requires users to render their own suspense boundaries
99
- Updates `Deferred` to use a true error boundary to catch render errors as well as data errors
1010
- Support array and single promise usages
11-
- `return deferred([ await critical(), lazy() ])`
12-
- `return deferred(lazy())`
11+
- `return defer([ await critical(), lazy() ])`
12+
- `return defer(lazy())`
1313
- Remove `Deferrable`/`ResolvedDeferrable` in favor of raw `Promise`'s and `Awaited`
14-
- Remove generics from `useDeferredData` until `useLoaderData` generic is decided in 6.5
14+
- Remove generics from `useAsyncValue` until `useLoaderData` generic is decided in 6.5

.changeset/sweet-books-switch.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"react-router": patch
3+
"@remix-run/router": patch
4+
---
5+
6+
fix: Rename `<Deferred>` to `<Await>`
7+
8+
- We are no longer replacing the `Promise` on `loaderData` with the value/error
9+
when it settles so it's now always a `Promise`.
10+
- To that end, we changed from `<Deferred value={promise}>` to
11+
`<Await resolve={promise}>` for clarity, and it also now supports using
12+
`<Await>` with raw promises from anywhere, not only those on `loaderData`
13+
from a defer() call.
14+
- The hook is also changed from `useDeferredData` -> `useAsyncValue`

examples/data-router/src/main.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
todoLoader,
2020
Todo,
2121
sleep,
22+
AwaitPage,
2223
} from "./routes";
2324
import "./index.css";
2425

@@ -39,6 +40,7 @@ ReactDOM.createRoot(document.getElementById("root")).render(
3940
element={<DeferredChild />}
4041
/>
4142
</Route>
43+
<Route id="await" path="await" element={<AwaitPage />} />
4244
<Route
4345
path="long-load"
4446
loader={() => sleep(3000)}

examples/data-router/src/routes.tsx

Lines changed: 86 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import React from "react";
22
import {
3-
type ActionFunction,
4-
type LoaderFunction,
5-
Deferred,
3+
Await,
64
Form,
75
Link,
86
Outlet,
9-
deferred,
10-
useDeferredData,
7+
defer,
8+
useAsyncError,
9+
useAsyncValue,
1110
useFetcher,
1211
useFetchers,
1312
useLoaderData,
@@ -17,12 +16,16 @@ import {
1716
useRouteError,
1817
json,
1918
useActionData,
19+
ActionFunctionArgs,
20+
LoaderFunctionArgs,
2021
} from "react-router-dom";
2122

2223
import type { Todos } from "./todos";
2324
import { addTodo, deleteTodo, getTodos } from "./todos";
2425

25-
export const sleep = (n: number = 500) => new Promise((r) => setTimeout(r, n));
26+
export function sleep(n: number = 500) {
27+
return new Promise((r) => setTimeout(r, n));
28+
}
2629

2730
export function Fallback() {
2831
return <p>Performing initial data "load"</p>;
@@ -47,6 +50,8 @@ export function Layout() {
4750
&nbsp;|&nbsp;
4851
<Link to="/deferred/child">Deferred Child</Link>
4952
&nbsp;|&nbsp;
53+
<Link to="/await">Await</Link>
54+
&nbsp;|&nbsp;
5055
<Link to="/long-load">Long Load</Link>
5156
&nbsp;|&nbsp;
5257
<Link to="/404">404 Link</Link>
@@ -79,12 +84,12 @@ interface HomeLoaderData {
7984
date: string;
8085
}
8186

82-
export const homeLoader: LoaderFunction = async (): Promise<HomeLoaderData> => {
87+
export async function homeLoader(): Promise<HomeLoaderData> {
8388
await sleep();
8489
return {
8590
date: new Date().toISOString(),
8691
};
87-
};
92+
}
8893

8994
export function Home() {
9095
let data = useLoaderData() as HomeLoaderData;
@@ -97,7 +102,7 @@ export function Home() {
97102
}
98103

99104
// Todos
100-
export const todosAction: ActionFunction = async ({ request }) => {
105+
export async function todosAction({ request }: ActionFunctionArgs) {
101106
await sleep();
102107

103108
let formData = await request.formData();
@@ -121,12 +126,12 @@ export const todosAction: ActionFunction = async ({ request }) => {
121126
status: 302,
122127
headers: { Location: "/todos" },
123128
});
124-
};
129+
}
125130

126-
export const todosLoader: LoaderFunction = async (): Promise<Todos> => {
131+
export async function todosLoader(): Promise<Todos> {
127132
await sleep();
128133
return getTodos();
129-
};
134+
}
130135

131136
export function TodosList() {
132137
let todos = useLoaderData() as Todos;
@@ -211,9 +216,9 @@ export function TodoItem({ id, todo }: TodoItemProps) {
211216
}
212217

213218
// Todo
214-
export const todoLoader: LoaderFunction = async ({
219+
export async function todoLoader({
215220
params,
216-
}): Promise<string> => {
221+
}: LoaderFunctionArgs): Promise<string> {
217222
await sleep();
218223
let todos = getTodos();
219224
if (!params.id) {
@@ -224,7 +229,7 @@ export const todoLoader: LoaderFunction = async ({
224229
throw new Error(`Uh oh, I couldn't find a todo with id "${params.id}"`);
225230
}
226231
return todo;
227-
};
232+
}
228233

229234
export function Todo() {
230235
let params = useParams();
@@ -251,57 +256,63 @@ interface DeferredRouteLoaderData {
251256
const rand = () => Math.round(Math.random() * 100);
252257
const resolve = (d: string, ms: number) =>
253258
new Promise((r) => setTimeout(() => r(`${d} - ${rand()}`), ms));
254-
const reject = (d: string, ms: number) =>
255-
new Promise((_, r) => setTimeout(() => r(`${d} - ${rand()}`), ms));
259+
const reject = (d: Error | string, ms: number) =>
260+
new Promise((_, r) =>
261+
setTimeout(() => {
262+
if (d instanceof Error) {
263+
d.message += ` - ${rand()}`;
264+
} else {
265+
d += ` - ${rand()}`;
266+
}
267+
r(d);
268+
}, ms)
269+
);
256270

257-
export const deferredLoader: LoaderFunction = async ({ request }) => {
258-
return deferred({
271+
export async function deferredLoader() {
272+
return defer({
259273
critical1: await resolve("Critical 1", 250),
260274
critical2: await resolve("Critical 2", 500),
261275
lazyResolved: Promise.resolve("Lazy Data immediately resolved - " + rand()),
262276
lazy1: resolve("Lazy 1", 1000),
263277
lazy2: resolve("Lazy 2", 1500),
264278
lazy3: resolve("Lazy 3", 2000),
265-
lazyError: reject("Kaboom!", 2500),
279+
lazyError: reject(new Error("Kaboom!"), 2500),
266280
});
267-
};
281+
}
268282

269283
export function DeferredPage() {
270284
let data = useLoaderData() as DeferredRouteLoaderData;
271-
272285
return (
273286
<div>
274287
<p>{data.critical1}</p>
275288
<p>{data.critical2}</p>
276289

277290
<React.Suspense fallback={<p>should not see me!</p>}>
278-
<Deferred value={data.lazyResolved}>
279-
<RenderDeferredData />
280-
</Deferred>
291+
<Await resolve={data.lazyResolved}>
292+
<RenderAwaitedData />
293+
</Await>
281294
</React.Suspense>
282295

283296
<React.Suspense fallback={<p>loading 1...</p>}>
284-
<Deferred value={data.lazy1}>
285-
<RenderDeferredData />
286-
</Deferred>
297+
<Await resolve={data.lazy1}>
298+
<RenderAwaitedData />
299+
</Await>
287300
</React.Suspense>
288301

289302
<React.Suspense fallback={<p>loading 2...</p>}>
290-
<Deferred value={data.lazy2}>
291-
<RenderDeferredData />
292-
</Deferred>
303+
<Await resolve={data.lazy2}>
304+
<RenderAwaitedData />
305+
</Await>
293306
</React.Suspense>
294307

295308
<React.Suspense fallback={<p>loading 3...</p>}>
296-
<Deferred value={data.lazy3}>
297-
{(data: string) => <p>{data}</p>}
298-
</Deferred>
309+
<Await resolve={data.lazy3}>{(data: string) => <p>{data}</p>}</Await>
299310
</React.Suspense>
300311

301312
<React.Suspense fallback={<p>loading (error)...</p>}>
302-
<Deferred value={data.lazyError} errorElement={<RenderDeferredError />}>
303-
<RenderDeferredData />
304-
</Deferred>
313+
<Await resolve={data.lazyError} errorElement={<RenderAwaitedError />}>
314+
<RenderAwaitedData />
315+
</Await>
305316
</React.Suspense>
306317

307318
<Outlet />
@@ -314,16 +325,16 @@ interface DeferredChildLoaderData {
314325
lazy: Promise<string>;
315326
}
316327

317-
export const deferredChildLoader: LoaderFunction = async ({ request }) => {
318-
return deferred({
328+
export async function deferredChildLoader() {
329+
return defer({
319330
critical: await resolve("Critical Child Data", 500),
320331
lazy: resolve("Lazy Child Data", 1000),
321332
});
322-
};
333+
}
323334

324-
export const deferredChildAction: ActionFunction = async ({ request }) => {
335+
export async function deferredChildAction() {
325336
return json({ ok: true });
326-
};
337+
}
327338

328339
export function DeferredChild() {
329340
let data = useLoaderData() as DeferredChildLoaderData;
@@ -332,9 +343,9 @@ export function DeferredChild() {
332343
<div>
333344
<p>{data.critical}</p>
334345
<React.Suspense fallback={<p>loading child...</p>}>
335-
<Deferred value={data.lazy}>
336-
<RenderDeferredData />
337-
</Deferred>
346+
<Await resolve={data.lazy}>
347+
<RenderAwaitedData />
348+
</Await>
338349
</React.Suspense>
339350
<Form method="post">
340351
<button type="submit" name="key" value="value">
@@ -346,13 +357,39 @@ export function DeferredChild() {
346357
);
347358
}
348359

349-
export function RenderDeferredData() {
350-
let data = useDeferredData<string>();
360+
let shouldResolve = true;
361+
let rawPromiseResolver: ((value: unknown) => void) | null;
362+
let rawPromiseRejecter: ((value: unknown) => void) | null;
363+
let rawPromise: Promise<unknown> = new Promise((r, j) => {
364+
rawPromiseResolver = r;
365+
rawPromiseRejecter = j;
366+
});
367+
368+
export function AwaitPage() {
369+
React.useEffect(() => {
370+
setTimeout(() => {
371+
if (shouldResolve) {
372+
rawPromiseResolver?.("Resolved raw promise!");
373+
} else {
374+
rawPromiseRejecter?.("Rejected raw promise!");
375+
}
376+
}, 1000);
377+
}, []);
378+
379+
return (
380+
<React.Suspense fallback={<p>Awaiting raw promise </p>}>
381+
<Await resolve={rawPromise}>{(data: string) => <p>{data}</p>}</Await>
382+
</React.Suspense>
383+
);
384+
}
385+
386+
function RenderAwaitedData() {
387+
let data = useAsyncValue() as string;
351388
return <p>{data}</p>;
352389
}
353390

354-
export function RenderDeferredError() {
355-
let error = useRouteError() as Error;
391+
function RenderAwaitedError() {
392+
let error = useAsyncError() as Error;
356393
return (
357394
<p style={{ color: "red" }}>
358395
Error (errorElement)!

0 commit comments

Comments
 (0)