Skip to content

Commit 3b16fb1

Browse files
fix(start): fix usage of use() with React 19
both router and react 19 were modifying the Promise object in a similar manner which caused a conflict. See https://github.com/facebook/react/blob/v19.0.0/packages/react-reconciler/src/ReactFiberThenable.js#L162-L228 for how React 19 modifies the Promise object. Both used an additional `status` property to track the Promise status, but router was using the value "success" and React was using the value "fulfilled" to mark a fulfilled Promise. This change stores the router specific Promise tracking values using a symbol to avoid any potential collision. fixes #2953
1 parent 2663b94 commit 3b16fb1

File tree

12 files changed

+116
-117
lines changed

12 files changed

+116
-117
lines changed

docs/framework/react/api/router.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ title: Router API
5353
- Types
5454
- [`ActiveLinkOptions Type`](./router/ActiveLinkOptionsType.md)
5555
- [`AsyncRouteComponent Type`](./router/AsyncRouteComponentType.md)
56-
- [`DeferredPromise Type`](./router/DeferredPromiseType.md)
5756
- [`HistoryState Interface`](./router/historyStateInterface.md)
5857
- [`LinkOptions Type`](./router/LinkOptionsType.md)
5958
- [`LinkProps Type`](./router/LinkPropsType.md)

docs/framework/react/api/router/DeferredPromiseType.md

Lines changed: 0 additions & 29 deletions
This file was deleted.

docs/framework/react/api/router/awaitComponent.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,18 @@ title: Await component
44
---
55

66
The `Await` component is a component that suspends until the provided promise is resolved or rejected.
7+
This is only necessary for React 18.
8+
If you are using React 19, you can use the `use()` hook instead.
79

810
## Await props
911

1012
The `Await` component accepts the following props:
1113

1214
### `props.promise` prop
1315

14-
- Type: [`DeferredPromise<T>`](./DeferredPromiseType.md)
16+
- Type: `Promise<T>`
1517
- Required
16-
- The deferred promise to await.
18+
- The promise to await.
1719

1820
### `props.children` prop
1921

docs/framework/react/api/router/deferFunction.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ id: deferFunction
33
title: defer function
44
---
55

6+
> [!CAUTION]
7+
> You don't need to call `defer` manually anymore, Promises are handled automatically now.
8+
69
The `defer` function wraps a promise with a deferred state object that can be used to inspect the promise's state. This deferred promise can then be passed to the [`useAwaited`](./useAwaitedHook.md) hook or the [`<Await>`](./awaitComponent.md) component for suspending until the promise is resolved or rejected.
710

811
The `defer` function accepts a single argument, the `promise` to wrap with a deferred state object.
@@ -15,7 +18,7 @@ The `defer` function accepts a single argument, the `promise` to wrap with a def
1518

1619
## defer returns
1720

18-
- A [`DeferredPromise<T>`](./DeferredPromiseType.md) that can be passed to the [`useAwaited`](./useAwaitedHook.md) hook or the [`<Await>`](./awaitComponent.md) component.
21+
- A promise that can be passed to the [`useAwaited`](./useAwaitedHook.md) hook or the [`<Await>`](./awaitComponent.md) component.
1922

2023
## Examples
2124

docs/framework/react/api/router/useAwaitedHook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The `useAwaited` hook accepts a single argument, an `options` object.
1111

1212
### `options.promise` option
1313

14-
- Type: [`DeferredPromise<T>`](./DeferredPromiseType.md)
14+
- Type: `Promise<T>`
1515
- Required
1616
- The deferred promise to await.
1717

docs/framework/react/guide/deferred-data-loading.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ Deferred data loading is a pattern that allows the router to render the next loc
99

1010
If you are using a library like [TanStack Query](https://react-query.tanstack.com) or any other data fetching library, then deferred data loading works a bit differently. Skip ahead to the [Deferred Data Loading with External Libraries](#deferred-data-loading-with-external-libraries) section for more information.
1111

12-
## Deferred Data Loading with `defer` and `Await`
12+
## Deferred Data Loading with and `Await`
1313

14-
To defer slow or non-critical data, wrap an **unawaited/unresolved** promise in the `defer` function and return it anywhere in your loader response:
14+
To defer slow or non-critical data, return an **unawaited/unresolved** promise anywhere in your loader response:
1515

1616
```tsx
1717
// src/routes/posts.$postId.tsx
@@ -28,8 +28,7 @@ export const Route = createFileRoute('/posts/$postId')({
2828

2929
return {
3030
fastData,
31-
// Wrap the slow promise in `defer()`
32-
deferredSlowData: defer(slowDataPromise),
31+
deferredSlowData: slowDataPromise,
3332
}
3433
},
3534
})
@@ -71,6 +70,9 @@ The `Await` component resolves the promise by triggering the nearest suspense bo
7170

7271
If the promise is rejected, the `Await` component will throw the serialized error, which can be caught by the nearest error boundary.
7372

73+
> [!TIP]
74+
> In React 19, you can use the `use()` hook instead of `Await`
75+
7476
## Deferred Data Loading with External libraries
7577

7678
When your strategy for fetching information for the route relies on [External Data Loading](./external-data-loading.md) with an external library like [TanStack Query](https://react-query.tanstack.com), deferred data loading works a bit differently, as the library handles the data fetching and caching for you outside of TanStack Router.
@@ -142,7 +144,7 @@ Please read the entire [SSR Guide](/docs/guide/server-streaming) for step by ste
142144
The following is a high-level overview of how deferred data streaming works with TanStack Router:
143145

144146
- Server
145-
- Promises wrapped in `defer()` are marked and tracked as they are returned from route loaders
147+
- Promises are marked and tracked as they are returned from route loaders
146148
- All loaders resolve and any deferred promises are serialized and embedded into the html
147149
- The route begins to render
148150
- Deferred promises rendered with the `<Await>` component trigger suspense boundaries, allowing the server to stream html up to that point

examples/react/basic-ssr-streaming-file-based/src/routes/error.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Await, createFileRoute, defer } from '@tanstack/react-router'
1+
import { Await, createFileRoute } from '@tanstack/react-router'
22
import * as React from 'react'
33

44
async function loadData() {
@@ -9,10 +9,10 @@ async function loadData() {
99

1010
export const Route = createFileRoute('/error')({
1111
component: ErrorComponent,
12-
loader: async () => {
12+
loader: () => {
1313
if (Math.random() > 0.5) throw new Error('Random error!')
1414
return {
15-
deferredData: defer(loadData()),
15+
deferredData: loadData(),
1616
}
1717
},
1818
pendingComponent: () => <p>Loading..</p>,

packages/react-router/src/awaited.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as React from 'react'
22
import warning from 'tiny-warning'
33
import { useRouter } from './useRouter'
44
import { defaultSerializeError } from './router'
5-
import { defer } from './defer'
5+
import { TSR_DEFERRED_PROMISE, defer } from './defer'
66
import { defaultDeserializeError, isServerSideError } from './isServerSideError'
77
import type { DeferredPromise } from './defer'
88

@@ -14,38 +14,36 @@ export function useAwaited<T>({
1414
promise: _promise,
1515
}: AwaitOptions<T>): [T, DeferredPromise<T>] {
1616
const router = useRouter()
17-
const promise = _promise as DeferredPromise<T>
17+
const promise = defer(_promise)
1818

19-
defer(promise)
20-
21-
if (promise.status === 'pending') {
19+
if (promise[TSR_DEFERRED_PROMISE].status === 'pending') {
2220
throw promise
2321
}
2422

25-
if (promise.status === 'error') {
23+
if (promise[TSR_DEFERRED_PROMISE].status === 'error') {
2624
if (typeof document !== 'undefined') {
27-
if (isServerSideError(promise.error)) {
25+
if (isServerSideError(promise[TSR_DEFERRED_PROMISE].error)) {
2826
throw (
2927
router.options.errorSerializer?.deserialize ?? defaultDeserializeError
30-
)(promise.error.data as any)
28+
)(promise[TSR_DEFERRED_PROMISE].error.data as any)
3129
} else {
3230
warning(
3331
false,
3432
"Encountered a server-side error that doesn't fit the expected shape",
3533
)
36-
throw promise.error
34+
throw promise[TSR_DEFERRED_PROMISE].error
3735
}
3836
} else {
3937
throw {
4038
data: (
4139
router.options.errorSerializer?.serialize ?? defaultSerializeError
42-
)(promise.error),
40+
)(promise[TSR_DEFERRED_PROMISE].error),
4341
__isServerError: true,
4442
}
4543
}
4644
}
47-
48-
return [promise.data as any, promise]
45+
console.log('useAwaited', promise[TSR_DEFERRED_PROMISE])
46+
return [promise[TSR_DEFERRED_PROMISE].data, promise]
4947
}
5048

5149
export function Await<T>(
@@ -68,5 +66,7 @@ function AwaitInner<T>(
6866
},
6967
): React.JSX.Element {
7068
const [data] = useAwaited(props)
69+
console.log('AwaitInner', data)
70+
7171
return props.children(data) as React.JSX.Element
7272
}

packages/react-router/src/defer.ts

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,24 @@
11
import { defaultSerializeError } from './router'
22

3+
export const TSR_DEFERRED_PROMISE = Symbol.for('TSR_DEFERRED_PROMISE')
4+
35
export type DeferredPromiseState<T> = {
4-
uid: string
5-
resolve?: () => void
6-
reject?: () => void
7-
} & (
8-
| {
9-
status: 'pending'
10-
data?: T
11-
error?: unknown
12-
}
13-
| {
14-
status: 'success'
15-
data: T
16-
}
17-
| {
18-
status: 'error'
19-
data?: T
20-
error: unknown
21-
}
22-
)
6+
[TSR_DEFERRED_PROMISE]:
7+
| {
8+
status: 'pending'
9+
data?: T
10+
error?: unknown
11+
}
12+
| {
13+
status: 'success'
14+
data: T
15+
}
16+
| {
17+
status: 'error'
18+
data?: T
19+
error: unknown
20+
}
21+
}
2322

2423
export type DeferredPromise<T> = Promise<T> & DeferredPromiseState<T>
2524

@@ -30,25 +29,25 @@ export function defer<T>(
3029
},
3130
) {
3231
const promise = _promise as DeferredPromise<T>
32+
// this is already deferred promise
33+
if ((promise as any)[TSR_DEFERRED_PROMISE]) {
34+
return promise
35+
}
36+
promise[TSR_DEFERRED_PROMISE] = { status: 'pending' }
3337

34-
if (!(promise as any).status) {
35-
Object.assign(promise, {
36-
status: 'pending',
38+
promise
39+
.then((data) => {
40+
promise[TSR_DEFERRED_PROMISE].status = 'success'
41+
promise[TSR_DEFERRED_PROMISE].data = data
42+
console.log('defer then', promise[TSR_DEFERRED_PROMISE])
43+
})
44+
.catch((error) => {
45+
promise[TSR_DEFERRED_PROMISE].status = 'error'
46+
;(promise[TSR_DEFERRED_PROMISE] as any).error = {
47+
data: (options?.serializeError ?? defaultSerializeError)(error),
48+
__isServerError: true,
49+
}
3750
})
38-
39-
promise
40-
.then((data) => {
41-
promise.status = 'success' as any
42-
promise.data = data
43-
})
44-
.catch((error) => {
45-
promise.status = 'error' as any
46-
;(promise as any).error = {
47-
data: (options?.serializeError ?? defaultSerializeError)(error),
48-
__isServerError: true,
49-
}
50-
})
51-
}
5251

5352
return promise
5453
}

packages/react-router/src/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export type { AwaitOptions } from './awaited'
1919

2020
export { ScriptOnce } from './ScriptOnce'
2121

22-
export { defer } from './defer'
22+
export { defer, TSR_DEFERRED_PROMISE } from './defer'
2323
export type { DeferredPromiseState, DeferredPromise } from './defer'
2424

2525
export { CatchBoundary, ErrorComponent } from './CatchBoundary'
@@ -262,6 +262,8 @@ export type {
262262
RouterListener,
263263
AnyRouterWithContext,
264264
ExtractedEntry,
265+
ExtractedStream,
266+
ExtractedPromise,
265267
StreamState,
266268
} from './router'
267269

packages/react-router/src/router.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
import { isRedirect, isResolvedRedirect } from './redirects'
3131
import { isNotFound } from './not-found'
3232
import { defaultTransformer } from './transformer'
33+
import type { DeferredPromiseState } from './defer'
3334
import type * as React from 'react'
3435
import type {
3536
HistoryLocation,
@@ -145,16 +146,26 @@ export type InferRouterContext<TRouteTree extends AnyRoute> =
145146
? TRouterContext
146147
: AnyContext
147148

148-
export type ExtractedEntry = {
149+
export interface ExtractedBaseEntry {
149150
dataType: '__beforeLoadContext' | 'loaderData'
150-
type: 'promise' | 'stream'
151+
type: string
151152
path: Array<string>
152-
value: any
153153
id: number
154-
streamState?: StreamState
155154
matchIndex: number
156155
}
157156

157+
export interface ExtractedStream extends ExtractedBaseEntry {
158+
type: 'stream'
159+
streamState: StreamState
160+
}
161+
162+
export interface ExtractedPromise extends ExtractedBaseEntry {
163+
type: 'promise'
164+
promiseState: DeferredPromiseState<any>
165+
}
166+
167+
export type ExtractedEntry = ExtractedStream | ExtractedPromise
168+
158169
export type StreamState = {
159170
promises: Array<ControlledPromise<string | null>>
160171
}

0 commit comments

Comments
 (0)