Skip to content

Commit b425b40

Browse files
Replace custom Thenable type with native Promises (#58337)
Next.js's implementation includes a custom Thenable type based on a similar one used in React's codebase. It was used to implement a userspace equivalent of the React.use API before that API became stable, by throwing a promise-like object and tracking the status on an expando field. However, it didn't cover all the same cases and behaviors that React.use does, which led to some subtle bugs like the one fixed by @ztanner in #55690. Now that React.use is stable, and we use that for suspending instead of throwing a promise, we no longer need our custom Thenable type. I've removed the type and associated functions, and updated our types to use Promise instead. Even in cases where a function does return a thenable-object rather than a native promise, like React Flight's createFromFetch, we should use TypeScript's built-in PromiseLike utility. Currently, though, we always await these objects anyway (in fetch-server-response.ts), which turns them into promises. So Promise is almost always sufficient. Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 00f5b5e commit b425b40

16 files changed

+70
-328
lines changed

packages/next/src/client/components/layout-router.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { RedirectBoundary } from './redirect-boundary'
2727
import { NotFoundBoundary } from './not-found-boundary'
2828
import { getSegmentValue } from './router-reducer/reducers/get-segment-value'
2929
import { createRouterCacheKey } from './router-reducer/create-router-cache-key'
30-
import { createRecordFromThenable } from './router-reducer/create-record-from-thenable'
3130

3231
/**
3332
* Add refetch marker to router state at the point of the current layout segment.
@@ -379,13 +378,11 @@ function InnerLayoutRouter({
379378

380379
childNode = {
381380
status: CacheStates.DATA_FETCH,
382-
data: createRecordFromThenable(
383-
fetchServerResponse(
384-
new URL(url, location.origin),
385-
refetchTree,
386-
context.nextUrl,
387-
buildId
388-
)
381+
data: fetchServerResponse(
382+
new URL(url, location.origin),
383+
refetchTree,
384+
context.nextUrl,
385+
buildId
389386
),
390387
subTreeData: null,
391388
head:

packages/next/src/client/components/promise-queue.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type { ThenableRecord } from './router-reducer/router-reducer-types'
2-
31
/*
42
This is a simple promise queue that allows you to limit the number of concurrent promises
53
that are running at any given time. It's used to limit the number of concurrent
@@ -10,7 +8,7 @@ export class PromiseQueue {
108
#maxConcurrency: number
119
#runningCount: number
1210
#queue: Array<{
13-
promiseFn: ThenableRecord<any> | Promise<any>
11+
promiseFn: Promise<any>
1412
task: () => void
1513
}>
1614

@@ -50,7 +48,7 @@ export class PromiseQueue {
5048
return taskPromise
5149
}
5250

53-
bump(promiseFn: ThenableRecord<any> | Promise<any>) {
51+
bump(promiseFn: Promise<any>) {
5452
const index = this.#queue.findIndex((item) => item.promiseFn === promiseFn)
5553

5654
if (index > -1) {

packages/next/src/client/components/router-reducer/create-record-from-thenable.test.ts

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

packages/next/src/client/components/router-reducer/create-record-from-thenable.ts

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

packages/next/src/client/components/router-reducer/fill-cache-with-data-property.test.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,16 @@ import type { FetchServerResponseResult } from './fetch-server-response'
33
import { fillCacheWithDataProperty } from './fill-cache-with-data-property'
44
import { CacheStates } from '../../../shared/lib/app-router-context.shared-runtime'
55
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
6-
import { createRecordFromThenable } from './create-record-from-thenable'
7-
import type { ThenableRecord } from './router-reducer-types'
6+
87
describe('fillCacheWithDataProperty', () => {
98
it('should add data property', () => {
109
const fetchServerResponseMock: jest.Mock<
11-
ThenableRecord<FetchServerResponseResult>
10+
Promise<FetchServerResponseResult>
1211
> = jest.fn(() =>
13-
createRecordFromThenable(
14-
Promise.resolve([
15-
/* TODO-APP: replace with actual FlightData */ '',
16-
undefined,
17-
])
18-
)
12+
Promise.resolve([
13+
/* TODO-APP: replace with actual FlightData */ '',
14+
undefined,
15+
])
1916
)
2017
const pathname = '/dashboard/settings'
2118
const segments = pathname.split('/')
@@ -97,9 +94,7 @@ describe('fillCacheWithDataProperty', () => {
9794
</React.Fragment>,
9895
},
9996
"dashboard" => {
100-
"data": Promise {
101-
"status": "pending",
102-
},
97+
"data": Promise {},
10398
"parallelRoutes": Map {},
10499
"status": "DATAFETCH",
105100
"subTreeData": null,

packages/next/src/client/components/router-reducer/fill-cache-with-data-property.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { FetchServerResponseResult } from './fetch-server-response'
2-
import type { ThenableRecord } from './router-reducer-types'
32
import type { FlightSegmentPath } from '../../../server/app-render/types'
43
import { CacheStates } from '../../../shared/lib/app-router-context.shared-runtime'
54
import type { CacheNode } from '../../../shared/lib/app-router-context.shared-runtime'
@@ -12,7 +11,7 @@ export function fillCacheWithDataProperty(
1211
newCache: CacheNode,
1312
existingCache: CacheNode,
1413
flightSegmentPath: FlightSegmentPath,
15-
fetchResponse: () => ThenableRecord<FetchServerResponseResult>,
14+
fetchResponse: () => Promise<FetchServerResponseResult>,
1615
bailOnParallelRoutes: boolean = false
1716
): { bailOptimistic: boolean } | undefined {
1817
const isLastEntry = flightSegmentPath.length <= 2

packages/next/src/client/components/router-reducer/reducers/fast-refresh-reducer.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { fetchServerResponse } from '../fetch-server-response'
2-
import { createRecordFromThenable } from '../create-record-from-thenable'
32
import { createHrefFromUrl } from '../create-href-from-url'
43
import { applyRouterStatePatchToTree } from '../apply-router-state-patch-to-tree'
54
import { isNavigatingToNewRootLayout } from '../is-navigating-to-new-root-layout'
@@ -30,13 +29,11 @@ function fastRefreshReducerImpl(
3029
if (!cache.data) {
3130
// TODO-APP: verify that `href` is not an external url.
3231
// Fetch data from the root of the tree.
33-
cache.data = createRecordFromThenable(
34-
fetchServerResponse(
35-
new URL(href, origin),
36-
[state.tree[0], state.tree[1], state.tree[2], 'refetch'],
37-
state.nextUrl,
38-
state.buildId
39-
)
32+
cache.data = fetchServerResponse(
33+
new URL(href, origin),
34+
[state.tree[0], state.tree[1], state.tree[2], 'refetch'],
35+
state.nextUrl,
36+
state.buildId
4037
)
4138
}
4239

packages/next/src/client/components/router-reducer/reducers/navigate-reducer.test.tsx

Lines changed: 4 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -274,37 +274,7 @@ describe('navigateReducer', () => {
274274
"nextUrl": "/linking/about",
275275
"prefetchCache": Map {
276276
"/linking/about" => {
277-
"data": Promise {
278-
"status": "fulfilled",
279-
"value": [
280-
[
281-
[
282-
"children",
283-
"linking",
284-
"children",
285-
"about",
286-
[
287-
"about",
288-
{
289-
"children": [
290-
"__PAGE__",
291-
{},
292-
],
293-
},
294-
],
295-
<h1>
296-
About Page!
297-
</h1>,
298-
<React.Fragment>
299-
<title>
300-
About page!
301-
</title>
302-
</React.Fragment>,
303-
],
304-
],
305-
undefined,
306-
],
307-
},
277+
"data": Promise {},
308278
"kind": "temporary",
309279
"lastUsedTime": 1690329600000,
310280
"prefetchTime": 1690329600000,
@@ -973,37 +943,7 @@ describe('navigateReducer', () => {
973943
</React.Fragment>,
974944
},
975945
"about" => {
976-
"data": Promise {
977-
"status": "fulfilled",
978-
"value": [
979-
[
980-
[
981-
"children",
982-
"linking",
983-
"children",
984-
"about",
985-
[
986-
"about",
987-
{
988-
"children": [
989-
"__PAGE__",
990-
{},
991-
],
992-
},
993-
],
994-
<h1>
995-
About Page!
996-
</h1>,
997-
<React.Fragment>
998-
<title>
999-
About page!
1000-
</title>
1001-
</React.Fragment>,
1002-
],
1003-
],
1004-
undefined,
1005-
],
1006-
},
946+
"data": Promise {},
1007947
"parallelRoutes": Map {},
1008948
"status": "DATAFETCH",
1009949
"subTreeData": null,
@@ -1422,37 +1362,7 @@ describe('navigateReducer', () => {
14221362
"nextUrl": "/linking/about",
14231363
"prefetchCache": Map {
14241364
"/linking/about" => {
1425-
"data": Promise {
1426-
"status": "fulfilled",
1427-
"value": [
1428-
[
1429-
[
1430-
"children",
1431-
"linking",
1432-
"children",
1433-
"about",
1434-
[
1435-
"about",
1436-
{
1437-
"children": [
1438-
"__PAGE__",
1439-
{},
1440-
],
1441-
},
1442-
],
1443-
<h1>
1444-
About Page!
1445-
</h1>,
1446-
<React.Fragment>
1447-
<title>
1448-
About page!
1449-
</title>
1450-
</React.Fragment>,
1451-
],
1452-
],
1453-
undefined,
1454-
],
1455-
},
1365+
"data": Promise {},
14561366
"kind": "auto",
14571367
"lastUsedTime": null,
14581368
"prefetchTime": 1690329600000,
@@ -2063,37 +1973,7 @@ describe('navigateReducer', () => {
20631973
"nextUrl": "/linking/about",
20641974
"prefetchCache": Map {
20651975
"/linking/about" => {
2066-
"data": Promise {
2067-
"status": "fulfilled",
2068-
"value": [
2069-
[
2070-
[
2071-
"children",
2072-
"linking",
2073-
"children",
2074-
"about",
2075-
[
2076-
"about",
2077-
{
2078-
"children": [
2079-
"__PAGE__",
2080-
{},
2081-
],
2082-
},
2083-
],
2084-
<h1>
2085-
About Page!
2086-
</h1>,
2087-
<React.Fragment>
2088-
<title>
2089-
About page!
2090-
</title>
2091-
</React.Fragment>,
2092-
],
2093-
],
2094-
undefined,
2095-
],
2096-
},
1976+
"data": Promise {},
20971977
"kind": "temporary",
20981978
"lastUsedTime": 1690329600000,
20991979
"prefetchTime": 1690329600000,

0 commit comments

Comments
 (0)