Unify promise switch statements
There are two different switch statements that we use to unwrap a `use`-ed promise, but there really only needs to be one. This was a factoring artifact that arose because I implemented the yieldy `status` instrumentation thing before I implemented `use` (for promises that are thrown directly during render, which is the old Suspense pattern that will be superseded by `use`).
Andrew Clark committed
Oct 22, 2022 at 20:39 UTC
fa77f52e74d816f3d7ad0e71850932508c1b353e
8 files changed
+222
-407
packages/react-reconciler/src/ReactFiberHooks.new.js
+1
-56
@@ -137,7 +137,6 @@ import {now} from './Scheduler';
137
import {
138
prepareThenableState,
139
trackUsedThenable,
140
- getPreviouslyUsedThenableAtIndex,
140
} from './ReactFiberThenable.new';
141
142
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
775
};
776
}
777
779
-function noop(): void {}
780
-
778
function use<T>(usable: Usable<T>): T {
779
if (usable !== null && typeof usable === 'object') {
780
// $FlowFixMe[method-unbinding]
@@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
785
// Track the position of the thenable within this fiber.
786
const index = thenableIndexCounter;
787
thenableIndexCounter += 1;
791
-
792
- // TODO: Unify this switch statement with the one in trackUsedThenable.
793
- switch (thenable.status) {
794
- case 'fulfilled': {
795
- const fulfilledValue: T = thenable.value;
796
- return fulfilledValue;
797
- }
798
- case 'rejected': {
799
- const rejectedError = thenable.reason;
800
- throw rejectedError;
801
- }
802
- default: {
803
- const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
804
- index,
805
- );
806
- if (prevThenableAtIndex !== null) {
807
- if (thenable !== prevThenableAtIndex) {
808
- // Avoid an unhandled rejection errors for the Promises that we'll
809
- // intentionally ignore.
810
- thenable.then(noop, noop);
811
- }
812
- switch (prevThenableAtIndex.status) {
813
- case 'fulfilled': {
814
- const fulfilledValue: T = prevThenableAtIndex.value;
815
- return fulfilledValue;
816
- }
817
- case 'rejected': {
818
- const rejectedError: mixed = prevThenableAtIndex.reason;
819
- throw rejectedError;
820
- }
821
- default: {
822
- // The thenable still hasn't resolved. Suspend with the same
823
- // thenable as last time to avoid redundant listeners.
824
- throw prevThenableAtIndex;
825
- }
826
- }
827
- } else {
828
- // This is the first time something has been used at this index.
829
- // Stash the thenable at the current index so we can reuse it during
830
- // the next attempt.
831
- trackUsedThenable(thenable, index);
832
-
833
- // Suspend.
834
- // TODO: Throwing here is an implementation detail that allows us to
835
- // unwind the call stack. But we shouldn't allow it to leak into
836
- // userspace. Throw an opaque placeholder value instead of the
837
- // actual thenable. If it doesn't get captured by the work loop, log
838
- // a warning, because that means something in userspace must have
839
- // caught it.
840
- throw thenable;
841
- }
842
- }
843
- }
788
+ return trackUsedThenable(thenable, index);
789
} else if (
790
usable.$$typeof === REACT_CONTEXT_TYPE ||
791
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberHooks.old.js
+1
-56
@@ -137,7 +137,6 @@ import {now} from './Scheduler';
137
import {
138
prepareThenableState,
139
trackUsedThenable,
140
- getPreviouslyUsedThenableAtIndex,
140
} from './ReactFiberThenable.old';
141
142
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -776,8 +775,6 @@ if (enableUseMemoCacheHook) {
775
};
776
}
777
779
-function noop(): void {}
780
-
778
function use<T>(usable: Usable<T>): T {
779
if (usable !== null && typeof usable === 'object') {
780
// $FlowFixMe[method-unbinding]
@@ -788,59 +785,7 @@ function use<T>(usable: Usable<T>): T {
785
// Track the position of the thenable within this fiber.
786
const index = thenableIndexCounter;
787
thenableIndexCounter += 1;
791
-
792
- // TODO: Unify this switch statement with the one in trackUsedThenable.
793
- switch (thenable.status) {
794
- case 'fulfilled': {
795
- const fulfilledValue: T = thenable.value;
796
- return fulfilledValue;
797
- }
798
- case 'rejected': {
799
- const rejectedError = thenable.reason;
800
- throw rejectedError;
801
- }
802
- default: {
803
- const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
804
- index,
805
- );
806
- if (prevThenableAtIndex !== null) {
807
- if (thenable !== prevThenableAtIndex) {
808
- // Avoid an unhandled rejection errors for the Promises that we'll
809
- // intentionally ignore.
810
- thenable.then(noop, noop);
811
- }
812
- switch (prevThenableAtIndex.status) {
813
- case 'fulfilled': {
814
- const fulfilledValue: T = prevThenableAtIndex.value;
815
- return fulfilledValue;
816
- }
817
- case 'rejected': {
818
- const rejectedError: mixed = prevThenableAtIndex.reason;
819
- throw rejectedError;
820
- }
821
- default: {
822
- // The thenable still hasn't resolved. Suspend with the same
823
- // thenable as last time to avoid redundant listeners.
824
- throw prevThenableAtIndex;
825
- }
826
- }
827
- } else {
828
- // This is the first time something has been used at this index.
829
- // Stash the thenable at the current index so we can reuse it during
830
- // the next attempt.
831
- trackUsedThenable(thenable, index);
832
-
833
- // Suspend.
834
- // TODO: Throwing here is an implementation detail that allows us to
835
- // unwind the call stack. But we shouldn't allow it to leak into
836
- // userspace. Throw an opaque placeholder value instead of the
837
- // actual thenable. If it doesn't get captured by the work loop, log
838
- // a warning, because that means something in userspace must have
839
- // caught it.
840
- throw thenable;
841
- }
842
- }
843
- }
788
+ return trackUsedThenable(thenable, index);
789
} else if (
790
usable.$$typeof === REACT_CONTEXT_TYPE ||
791
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-reconciler/src/ReactFiberThenable.new.js
+53
-43
@@ -17,8 +17,7 @@ import type {
17
import ReactSharedInternals from 'shared/ReactSharedInternals';
18
const {ReactCurrentActQueue} = ReactSharedInternals;
19
20
-// TODO: Sparse arrays are bad for performance.
21
-export opaque type ThenableState = Array<Thenable<any> | void>;
20
+export opaque type ThenableState = Array<Thenable<any>>;
21
22
let thenableState: ThenableState | null = null;
23
@@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
61
return true;
62
}
63
65
-export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
64
+function noop(): void {}
65
+
66
+export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
67
if (__DEV__ && ReactCurrentActQueue.current !== null) {
68
ReactCurrentActQueue.didUsePromise = true;
69
}
@@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
71
if (thenableState === null) {
72
thenableState = [thenable];
73
} else {
73
- thenableState[index] = thenable;
74
+ const previous = thenableState[index];
75
+ if (previous === undefined) {
76
+ thenableState.push(thenable);
77
+ } else {
78
+ if (previous !== thenable) {
79
+ // Reuse the previous thenable, and drop the new one. We can assume
80
+ // they represent the same value, because components are idempotent.
81
+
82
+ // Avoid an unhandled rejection errors for the Promises that we'll
83
+ // intentionally ignore.
84
+ thenable.then(noop, noop);
85
+ thenable = previous;
86
+ }
87
+ }
88
}
89
90
// We use an expando to track the status and result of a thenable so that we
@@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
94
// If the thenable doesn't have a status, set it to "pending" and attach
95
// a listener that will update its status and result when it resolves.
96
switch (thenable.status) {
83
- case 'fulfilled':
84
- case 'rejected':
85
- // A thenable that already resolved shouldn't have been thrown, so this is
86
- // unexpected. Suggests a mistake in a userspace data library. Don't track
87
- // this thenable, because if we keep trying it will likely infinite loop
88
- // without ever resolving.
89
- // TODO: Log a warning?
90
- break;
97
+ case 'fulfilled': {
98
+ const fulfilledValue: T = thenable.value;
99
+ return fulfilledValue;
100
+ }
101
+ case 'rejected': {
102
+ const rejectedError = thenable.reason;
103
+ throw rejectedError;
104
+ }
105
default: {
106
if (typeof thenable.status === 'string') {
107
// Only instrument the thenable if the status if not defined. If
108
// it's defined, but an unknown value, assume it's been instrumented by
109
// some custom userspace implementation. We treat it as "pending".
96
- break;
110
+ } else {
111
+ const pendingThenable: PendingThenable<mixed> = (thenable: any);
112
+ pendingThenable.status = 'pending';
113
+ pendingThenable.then(
114
+ fulfilledValue => {
115
+ if (thenable.status === 'pending') {
116
+ const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
117
+ fulfilledThenable.status = 'fulfilled';
118
+ fulfilledThenable.value = fulfilledValue;
119
+ }
120
+ },
121
+ (error: mixed) => {
122
+ if (thenable.status === 'pending') {
123
+ const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
124
+ rejectedThenable.status = 'rejected';
125
+ rejectedThenable.reason = error;
126
+ }
127
+ },
128
+ );
129
}
98
- const pendingThenable: PendingThenable<mixed> = (thenable: any);
99
- pendingThenable.status = 'pending';
100
- pendingThenable.then(
101
- fulfilledValue => {
102
- if (thenable.status === 'pending') {
103
- const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
104
- fulfilledThenable.status = 'fulfilled';
105
- fulfilledThenable.value = fulfilledValue;
106
- }
107
- },
108
- (error: mixed) => {
109
- if (thenable.status === 'pending') {
110
- const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
111
- rejectedThenable.status = 'rejected';
112
- rejectedThenable.reason = error;
113
- }
114
- },
115
- );
116
- break;
117
- }
118
- }
119
-}
130
121
-export function getPreviouslyUsedThenableAtIndex<T>(
122
- index: number,
123
-): Thenable<T> | null {
124
- if (thenableState !== null) {
125
- const thenable = thenableState[index];
126
- if (thenable !== undefined) {
127
- return thenable;
131
+ // Suspend.
132
+ // TODO: Throwing here is an implementation detail that allows us to
133
+ // unwind the call stack. But we shouldn't allow it to leak into
134
+ // userspace. Throw an opaque placeholder value instead of the
135
+ // actual thenable. If it doesn't get captured by the work loop, log
136
+ // a warning, because that means something in userspace must have
137
+ // caught it.
138
+ throw thenable;
139
}
140
}
130
- return null;
141
}
packages/react-reconciler/src/ReactFiberThenable.old.js
+53
-43
@@ -17,8 +17,7 @@ import type {
17
import ReactSharedInternals from 'shared/ReactSharedInternals';
18
const {ReactCurrentActQueue} = ReactSharedInternals;
19
20
-// TODO: Sparse arrays are bad for performance.
21
-export opaque type ThenableState = Array<Thenable<any> | void>;
20
+export opaque type ThenableState = Array<Thenable<any>>;
21
22
let thenableState: ThenableState | null = null;
23
@@ -62,7 +61,9 @@ export function isThenableStateResolved(thenables: ThenableState): boolean {
61
return true;
62
}
63
65
-export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
64
+function noop(): void {}
65
+
66
+export function trackUsedThenable<T>(thenable: Thenable<T>, index: number): T {
67
if (__DEV__ && ReactCurrentActQueue.current !== null) {
68
ReactCurrentActQueue.didUsePromise = true;
69
}
@@ -70,7 +71,20 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
71
if (thenableState === null) {
72
thenableState = [thenable];
73
} else {
73
- thenableState[index] = thenable;
74
+ const previous = thenableState[index];
75
+ if (previous === undefined) {
76
+ thenableState.push(thenable);
77
+ } else {
78
+ if (previous !== thenable) {
79
+ // Reuse the previous thenable, and drop the new one. We can assume
80
+ // they represent the same value, because components are idempotent.
81
+
82
+ // Avoid an unhandled rejection errors for the Promises that we'll
83
+ // intentionally ignore.
84
+ thenable.then(noop, noop);
85
+ thenable = previous;
86
+ }
87
+ }
88
}
89
90
// We use an expando to track the status and result of a thenable so that we
@@ -80,52 +94,48 @@ export function trackUsedThenable<T>(thenable: Thenable<T>, index: number) {
94
// If the thenable doesn't have a status, set it to "pending" and attach
95
// a listener that will update its status and result when it resolves.
96
switch (thenable.status) {
83
- case 'fulfilled':
84
- case 'rejected':
85
- // A thenable that already resolved shouldn't have been thrown, so this is
86
- // unexpected. Suggests a mistake in a userspace data library. Don't track
87
- // this thenable, because if we keep trying it will likely infinite loop
88
- // without ever resolving.
89
- // TODO: Log a warning?
90
- break;
97
+ case 'fulfilled': {
98
+ const fulfilledValue: T = thenable.value;
99
+ return fulfilledValue;
100
+ }
101
+ case 'rejected': {
102
+ const rejectedError = thenable.reason;
103
+ throw rejectedError;
104
+ }
105
default: {
106
if (typeof thenable.status === 'string') {
107
// Only instrument the thenable if the status if not defined. If
108
// it's defined, but an unknown value, assume it's been instrumented by
109
// some custom userspace implementation. We treat it as "pending".
96
- break;
110
+ } else {
111
+ const pendingThenable: PendingThenable<mixed> = (thenable: any);
112
+ pendingThenable.status = 'pending';
113
+ pendingThenable.then(
114
+ fulfilledValue => {
115
+ if (thenable.status === 'pending') {
116
+ const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
117
+ fulfilledThenable.status = 'fulfilled';
118
+ fulfilledThenable.value = fulfilledValue;
119
+ }
120
+ },
121
+ (error: mixed) => {
122
+ if (thenable.status === 'pending') {
123
+ const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
124
+ rejectedThenable.status = 'rejected';
125
+ rejectedThenable.reason = error;
126
+ }
127
+ },
128
+ );
129
}
98
- const pendingThenable: PendingThenable<mixed> = (thenable: any);
99
- pendingThenable.status = 'pending';
100
- pendingThenable.then(
101
- fulfilledValue => {
102
- if (thenable.status === 'pending') {
103
- const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
104
- fulfilledThenable.status = 'fulfilled';
105
- fulfilledThenable.value = fulfilledValue;
106
- }
107
- },
108
- (error: mixed) => {
109
- if (thenable.status === 'pending') {
110
- const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
111
- rejectedThenable.status = 'rejected';
112
- rejectedThenable.reason = error;
113
- }
114
- },
115
- );
116
- break;
117
- }
118
- }
119
-}
130
121
-export function getPreviouslyUsedThenableAtIndex<T>(
122
- index: number,
123
-): Thenable<T> | null {
124
- if (thenableState !== null) {
125
- const thenable = thenableState[index];
126
- if (thenable !== undefined) {
127
- return thenable;
131
+ // Suspend.
132
+ // TODO: Throwing here is an implementation detail that allows us to
133
+ // unwind the call stack. But we shouldn't allow it to leak into
134
+ // userspace. Throw an opaque placeholder value instead of the
135
+ // actual thenable. If it doesn't get captured by the work loop, log
136
+ // a warning, because that means something in userspace must have
137
+ // caught it.
138
+ throw thenable;
139
}
140
}
130
- return null;
141
}
packages/react-server/src/ReactFizzHooks.js
+4
-60
@@ -25,11 +25,7 @@ import type {ThenableState} from './ReactFizzThenable';
25
26
import {readContext as readContextImpl} from './ReactFizzNewContext';
27
import {getTreeId} from './ReactFizzTreeContext';
28
-import {
29
- getPreviouslyUsedThenableAtIndex,
30
- createThenableState,
31
- trackUsedThenable,
32
-} from './ReactFizzThenable';
28
+import {createThenableState, trackUsedThenable} from './ReactFizzThenable';
29
30
import {makeId} from './ReactServerFormatConfig';
31
@@ -593,62 +589,10 @@ function use<T>(usable: Usable<T>): T {
589
const index = thenableIndexCounter;
590
thenableIndexCounter += 1;
591
596
- // TODO: Unify this switch statement with the one in trackUsedThenable.
597
- switch (thenable.status) {
598
- case 'fulfilled': {
599
- const fulfilledValue: T = thenable.value;
600
- return fulfilledValue;
601
- }
602
- case 'rejected': {
603
- const rejectedError = thenable.reason;
604
- throw rejectedError;
605
- }
606
- default: {
607
- const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
608
- thenableState,
609
- index,
610
- );
611
- if (prevThenableAtIndex !== null) {
612
- if (thenable !== prevThenableAtIndex) {
613
- // Avoid an unhandled rejection errors for the Promises that we'll
614
- // intentionally ignore.
615
- thenable.then(noop, noop);
616
- }
617
- switch (prevThenableAtIndex.status) {
618
- case 'fulfilled': {
619
- const fulfilledValue: T = prevThenableAtIndex.value;
620
- return fulfilledValue;
621
- }
622
- case 'rejected': {
623
- const rejectedError: mixed = prevThenableAtIndex.reason;
624
- throw rejectedError;
625
- }
626
- default: {
627
- // The thenable still hasn't resolved. Suspend with the same
628
- // thenable as last time to avoid redundant listeners.
629
- throw prevThenableAtIndex;
630
- }
631
- }
632
- } else {
633
- // This is the first time something has been used at this index.
634
- // Stash the thenable at the current index so we can reuse it during
635
- // the next attempt.
636
- if (thenableState === null) {
637
- thenableState = createThenableState();
638
- }
639
- trackUsedThenable(thenableState, thenable, index);
640
-
641
- // Suspend.
642
- // TODO: Throwing here is an implementation detail that allows us to
643
- // unwind the call stack. But we shouldn't allow it to leak into
644
- // userspace. Throw an opaque placeholder value instead of the
645
- // actual thenable. If it doesn't get captured by the work loop, log
646
- // a warning, because that means something in userspace must have
647
- // caught it.
648
- throw thenable;
649
- }
650
- }
592
+ if (thenableState === null) {
593
+ thenableState = createThenableState();
594
}
595
+ return trackUsedThenable(thenableState, thenable, index);
596
} else if (
597
usable.$$typeof === REACT_CONTEXT_TYPE ||
598
usable.$$typeof === REACT_SERVER_CONTEXT_TYPE
packages/react-server/src/ReactFizzThenable.js
+53
-44
@@ -20,8 +20,7 @@ import type {
20
RejectedThenable,
21
} from 'shared/ReactTypes';
22
23
-// TODO: Sparse arrays are bad for performance.
24
-export opaque type ThenableState = Array<Thenable<any> | void>;
23
+export opaque type ThenableState = Array<Thenable<any>>;
24
25
export function createThenableState(): ThenableState {
26
// The ThenableState is created the first time a component suspends. If it
@@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
28
return [];
29
}
30
31
+function noop(): void {}
32
+
33
export function trackUsedThenable<T>(
34
thenableState: ThenableState,
35
thenable: Thenable<T>,
36
index: number,
36
-) {
37
- thenableState[index] = thenable;
37
+): T {
38
+ const previous = thenableState[index];
39
+ if (previous === undefined) {
40
+ thenableState.push(thenable);
41
+ } else {
42
+ if (previous !== thenable) {
43
+ // Reuse the previous thenable, and drop the new one. We can assume
44
+ // they represent the same value, because components are idempotent.
45
+
46
+ // Avoid an unhandled rejection errors for the Promises that we'll
47
+ // intentionally ignore.
48
+ thenable.then(noop, noop);
49
+ thenable = previous;
50
+ }
51
+ }
52
53
// We use an expando to track the status and result of a thenable so that we
54
// can synchronously unwrap the value. Think of this as an extension of the
@@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
57
// If the thenable doesn't have a status, set it to "pending" and attach
58
// a listener that will update its status and result when it resolves.
59
switch (thenable.status) {
46
- case 'fulfilled':
47
- case 'rejected':
48
- // A thenable that already resolved shouldn't have been thrown, so this is
49
- // unexpected. Suggests a mistake in a userspace data library. Don't track
50
- // this thenable, because if we keep trying it will likely infinite loop
51
- // without ever resolving.
52
- // TODO: Log a warning?
53
- break;
60
+ case 'fulfilled': {
61
+ const fulfilledValue: T = thenable.value;
62
+ return fulfilledValue;
63
+ }
64
+ case 'rejected': {
65
+ const rejectedError = thenable.reason;
66
+ throw rejectedError;
67
+ }
68
default: {
69
if (typeof thenable.status === 'string') {
70
// Only instrument the thenable if the status if not defined. If
71
// it's defined, but an unknown value, assume it's been instrumented by
72
// some custom userspace implementation. We treat it as "pending".
59
- break;
73
+ } else {
74
+ const pendingThenable: PendingThenable<mixed> = (thenable: any);
75
+ pendingThenable.status = 'pending';
76
+ pendingThenable.then(
77
+ fulfilledValue => {
78
+ if (thenable.status === 'pending') {
79
+ const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
80
+ fulfilledThenable.status = 'fulfilled';
81
+ fulfilledThenable.value = fulfilledValue;
82
+ }
83
+ },
84
+ (error: mixed) => {
85
+ if (thenable.status === 'pending') {
86
+ const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
87
+ rejectedThenable.status = 'rejected';
88
+ rejectedThenable.reason = error;
89
+ }
90
+ },
91
+ );
92
}
61
- const pendingThenable: PendingThenable<mixed> = (thenable: any);
62
- pendingThenable.status = 'pending';
63
- pendingThenable.then(
64
- fulfilledValue => {
65
- if (thenable.status === 'pending') {
66
- const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
67
- fulfilledThenable.status = 'fulfilled';
68
- fulfilledThenable.value = fulfilledValue;
69
- }
70
- },
71
- (error: mixed) => {
72
- if (thenable.status === 'pending') {
73
- const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
74
- rejectedThenable.status = 'rejected';
75
- rejectedThenable.reason = error;
76
- }
77
- },
78
- );
79
- break;
80
- }
81
- }
82
-}
93
84
-export function getPreviouslyUsedThenableAtIndex<T>(
85
- thenableState: ThenableState | null,
86
- index: number,
87
-): Thenable<T> | null {
88
- if (thenableState !== null) {
89
- const thenable = thenableState[index];
90
- if (thenable !== undefined) {
91
- return thenable;
94
+ // Suspend.
95
+ // TODO: Throwing here is an implementation detail that allows us to
96
+ // unwind the call stack. But we shouldn't allow it to leak into
97
+ // userspace. Throw an opaque placeholder value instead of the
98
+ // actual thenable. If it doesn't get captured by the work loop, log
99
+ // a warning, because that means something in userspace must have
100
+ // caught it.
101
+ throw thenable;
102
}
103
}
94
- return null;
104
}
packages/react-server/src/ReactFlightHooks.js
+4
-61
@@ -17,11 +17,7 @@ import {
17
} from 'shared/ReactSymbols';
18
import {readContext as readContextImpl} from './ReactFlightNewContext';
19
import {enableUseHook} from 'shared/ReactFeatureFlags';
20
-import {
21
- getPreviouslyUsedThenableAtIndex,
22
- createThenableState,
23
- trackUsedThenable,
24
-} from './ReactFlightThenable';
20
+import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
21
22
let currentRequest = null;
23
let thenableIndexCounter = 0;
@@ -121,8 +117,6 @@ function useId(): string {
117
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
118
}
119
124
-function noop(): void {}
125
-
120
function use<T>(usable: Usable<T>): T {
121
if (usable !== null && typeof usable === 'object') {
122
// $FlowFixMe[method-unbinding]
@@ -134,61 +128,10 @@ function use<T>(usable: Usable<T>): T {
128
const index = thenableIndexCounter;
129
thenableIndexCounter += 1;
130
137
- switch (thenable.status) {
138
- case 'fulfilled': {
139
- const fulfilledValue: T = thenable.value;
140
- return fulfilledValue;
141
- }
142
- case 'rejected': {
143
- const rejectedError = thenable.reason;
144
- throw rejectedError;
145
- }
146
- default: {
147
- const prevThenableAtIndex: Thenable<T> | null = getPreviouslyUsedThenableAtIndex(
148
- thenableState,
149
- index,
150
- );
151
- if (prevThenableAtIndex !== null) {
152
- if (thenable !== prevThenableAtIndex) {
153
- // Avoid an unhandled rejection errors for the Promises that we'll
154
- // intentionally ignore.
155
- thenable.then(noop, noop);
156
- }
157
- switch (prevThenableAtIndex.status) {
158
- case 'fulfilled': {
159
- const fulfilledValue: T = prevThenableAtIndex.value;
160
- return fulfilledValue;
161
- }
162
- case 'rejected': {
163
- const rejectedError: mixed = prevThenableAtIndex.reason;
164
- throw rejectedError;
165
- }
166
- default: {
167
- // The thenable still hasn't resolved. Suspend with the same
168
- // thenable as last time to avoid redundant listeners.
169
- throw prevThenableAtIndex;
170
- }
171
- }
172
- } else {
173
- // This is the first time something has been used at this index.
174
- // Stash the thenable at the current index so we can reuse it during
175
- // the next attempt.
176
- if (thenableState === null) {
177
- thenableState = createThenableState();
178
- }
179
- trackUsedThenable(thenableState, thenable, index);
180
-
181
- // Suspend.
182
- // TODO: Throwing here is an implementation detail that allows us to
183
- // unwind the call stack. But we shouldn't allow it to leak into
184
- // userspace. Throw an opaque placeholder value instead of the
185
- // actual thenable. If it doesn't get captured by the work loop, log
186
- // a warning, because that means something in userspace must have
187
- // caught it.
188
- throw thenable;
189
- }
190
- }
131
+ if (thenableState === null) {
132
+ thenableState = createThenableState();
133
}
134
+ return trackUsedThenable(thenableState, thenable, index);
135
} else if (usable.$$typeof === REACT_SERVER_CONTEXT_TYPE) {
136
const context: ReactServerContext<T> = (usable: any);
137
return readContext(context);
packages/react-server/src/ReactFlightThenable.js
+53
-44
@@ -20,8 +20,7 @@ import type {
20
RejectedThenable,
21
} from 'shared/ReactTypes';
22
23
-// TODO: Sparse arrays are bad for performance.
24
-export opaque type ThenableState = Array<Thenable<any> | void>;
23
+export opaque type ThenableState = Array<Thenable<any>>;
24
25
export function createThenableState(): ThenableState {
26
// The ThenableState is created the first time a component suspends. If it
@@ -29,12 +28,27 @@ export function createThenableState(): ThenableState {
28
return [];
29
}
30
31
+function noop(): void {}
32
+
33
export function trackUsedThenable<T>(
34
thenableState: ThenableState,
35
thenable: Thenable<T>,
36
index: number,
36
-) {
37
- thenableState[index] = thenable;
37
+): T {
38
+ const previous = thenableState[index];
39
+ if (previous === undefined) {
40
+ thenableState.push(thenable);
41
+ } else {
42
+ if (previous !== thenable) {
43
+ // Reuse the previous thenable, and drop the new one. We can assume
44
+ // they represent the same value, because components are idempotent.
45
+
46
+ // Avoid an unhandled rejection errors for the Promises that we'll
47
+ // intentionally ignore.
48
+ thenable.then(noop, noop);
49
+ thenable = previous;
50
+ }
51
+ }
52
53
// We use an expando to track the status and result of a thenable so that we
54
// can synchronously unwrap the value. Think of this as an extension of the
@@ -43,53 +57,48 @@ export function trackUsedThenable<T>(
57
// If the thenable doesn't have a status, set it to "pending" and attach
58
// a listener that will update its status and result when it resolves.
59
switch (thenable.status) {
46
- case 'fulfilled':
47
- case 'rejected':
48
- // A thenable that already resolved shouldn't have been thrown, so this is
49
- // unexpected. Suggests a mistake in a userspace data library. Don't track
50
- // this thenable, because if we keep trying it will likely infinite loop
51
- // without ever resolving.
52
- // TODO: Log a warning?
53
- break;
60
+ case 'fulfilled': {
61
+ const fulfilledValue: T = thenable.value;
62
+ return fulfilledValue;
63
+ }
64
+ case 'rejected': {
65
+ const rejectedError = thenable.reason;
66
+ throw rejectedError;
67
+ }
68
default: {
69
if (typeof thenable.status === 'string') {
70
// Only instrument the thenable if the status if not defined. If
71
// it's defined, but an unknown value, assume it's been instrumented by
72
// some custom userspace implementation. We treat it as "pending".
59
- break;
73
+ } else {
74
+ const pendingThenable: PendingThenable<mixed> = (thenable: any);
75
+ pendingThenable.status = 'pending';
76
+ pendingThenable.then(
77
+ fulfilledValue => {
78
+ if (thenable.status === 'pending') {
79
+ const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
80
+ fulfilledThenable.status = 'fulfilled';
81
+ fulfilledThenable.value = fulfilledValue;
82
+ }
83
+ },
84
+ (error: mixed) => {
85
+ if (thenable.status === 'pending') {
86
+ const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
87
+ rejectedThenable.status = 'rejected';
88
+ rejectedThenable.reason = error;
89
+ }
90
+ },
91
+ );
92
}
61
- const pendingThenable: PendingThenable<mixed> = (thenable: any);
62
- pendingThenable.status = 'pending';
63
- pendingThenable.then(
64
- fulfilledValue => {
65
- if (thenable.status === 'pending') {
66
- const fulfilledThenable: FulfilledThenable<mixed> = (thenable: any);
67
- fulfilledThenable.status = 'fulfilled';
68
- fulfilledThenable.value = fulfilledValue;
69
- }
70
- },
71
- (error: mixed) => {
72
- if (thenable.status === 'pending') {
73
- const rejectedThenable: RejectedThenable<mixed> = (thenable: any);
74
- rejectedThenable.status = 'rejected';
75
- rejectedThenable.reason = error;
76
- }
77
- },
78
- );
79
- break;
80
- }
81
- }
82
-}
93
84
-export function getPreviouslyUsedThenableAtIndex<T>(
85
- thenableState: ThenableState | null,
86
- index: number,
87
-): Thenable<T> | null {
88
- if (thenableState !== null) {
89
- const thenable = thenableState[index];
90
- if (thenable !== undefined) {
91
- return thenable;
94
+ // Suspend.
95
+ // TODO: Throwing here is an implementation detail that allows us to
96
+ // unwind the call stack. But we shouldn't allow it to leak into
97
+ // userspace. Throw an opaque placeholder value instead of the
98
+ // actual thenable. If it doesn't get captured by the work loop, log
99
+ // a warning, because that means something in userspace must have
100
+ // caught it.
101
+ throw thenable;
102
}
103
}
94
- return null;
104
}