Implement useOpaqueIdentifier (#21260)
The format of this ID is specific to the format.
Sebastian Markbåge committed
Apr 14, 2021 at 17:25 UTC
f4d7a0f1ea0cd24e1979a9be6b2778b98cef414c
7 files changed
+62
-7
packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+20
-2
@@ -57,8 +57,9 @@ export type ResponseState = {
57
placeholderPrefix: PrecomputedChunk,
58
segmentPrefix: PrecomputedChunk,
59
boundaryPrefix: string,
60
- opaqueIdentifierPrefix: PrecomputedChunk,
60
+ opaqueIdentifierPrefix: string,
61
nextSuspenseID: number,
62
+ nextOpaqueID: number,
63
sentCompleteSegmentFunction: boolean,
64
sentCompleteBoundaryFunction: boolean,
65
sentClientRenderFunction: boolean,
@@ -72,8 +73,9 @@ export function createResponseState(
73
placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'),
74
segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'),
75
boundaryPrefix: identifierPrefix + 'B:',
75
- opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'),
76
+ opaqueIdentifierPrefix: identifierPrefix + 'R:',
77
nextSuspenseID: 0,
78
+ nextOpaqueID: 0,
79
sentCompleteSegmentFunction: false,
80
sentCompleteBoundaryFunction: false,
81
sentClientRenderFunction: false,
@@ -172,6 +174,22 @@ export function createSuspenseBoundaryID(
174
return {formattedID: null};
175
}
176
177
+export type OpaqueIDType = string;
178
+
179
+export function makeServerID(
180
+ responseState: null | ResponseState,
181
+): OpaqueIDType {
182
+ invariant(
183
+ responseState !== null,
184
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component.',
185
+ );
186
+ // TODO: This is not deterministic since it's created during render.
187
+ return (
188
+ responseState.opaqueIdentifierPrefix +
189
+ (responseState.nextOpaqueID++).toString(36)
190
+ );
191
+}
192
+
193
function encodeHTMLTextNode(text: string): string {
194
return escapeTextForBrowser(text);
195
}
packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+15
@@ -61,12 +61,14 @@ SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
61
// Per response,
62
export type ResponseState = {
63
nextSuspenseID: number,
64
+ nextOpaqueID: number,
65
};
66
67
// Allows us to keep track of what we've already written so we can refer back to it.
68
export function createResponseState(): ResponseState {
69
return {
70
nextSuspenseID: 0,
71
+ nextOpaqueID: 0,
72
};
73
}
74
@@ -108,6 +110,19 @@ export function createSuspenseBoundaryID(
110
return responseState.nextSuspenseID++;
111
}
112
113
+export type OpaqueIDType = number;
114
+
115
+export function makeServerID(
116
+ responseState: null | ResponseState,
117
+): OpaqueIDType {
118
+ invariant(
119
+ responseState !== null,
120
+ 'Invalid hook call. Hooks can only be called inside of the body of a function component.',
121
+ );
122
+ // TODO: This is not deterministic since it's created during render.
123
+ return responseState.nextOpaqueID++;
124
+}
125
+
126
const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
127
128
export function pushEmpty(
packages/react-noop-renderer/src/ReactNoopServer.js
+6
@@ -53,6 +53,8 @@ type Destination = {
53
54
const POP = Buffer.from('/', 'utf8');
55
56
+let opaqueID = 0;
57
+
58
const ReactNoopServer = ReactFizzServer({
59
scheduleWork(callback: () => void) {
60
callback();
@@ -84,6 +86,10 @@ const ReactNoopServer = ReactFizzServer({
86
return {state: 'pending', children: []};
87
},
88
89
+ makeServerID(): number {
90
+ return opaqueID++;
91
+ },
92
+
93
getChildFormatContext(): null {
94
return null;
95
},
packages/react-server/src/ReactFizzHooks.js
+12
-3
@@ -16,8 +16,12 @@ import type {
16
ReactContext,
17
} from 'shared/ReactTypes';
18
19
+import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
20
+
21
import {readContext as readContextImpl} from './ReactFizzNewContext';
22
23
+import {makeServerID} from './ReactServerFormatConfig';
24
+
25
import invariant from 'shared/invariant';
26
import {enableCache} from 'shared/ReactFeatureFlags';
27
import is from 'shared/objectIs';
@@ -41,8 +45,6 @@ type Hook = {|
45
next: Hook | null,
46
|};
47
44
-type OpaqueIDType = string;
45
-
48
let currentlyRenderingComponent: Object | null = null;
49
let firstWorkInProgressHook: Hook | null = null;
50
let workInProgressHook: Hook | null = null;
@@ -474,7 +476,7 @@ function useTransition(): [(callback: () => void) => void, boolean] {
476
}
477
478
function useOpaqueIdentifier(): OpaqueIDType {
477
- throw new Error('Not yet implemented.');
479
+ return makeServerID(currentResponseState);
480
}
481
482
function unsupportedRefresh() {
@@ -513,3 +515,10 @@ if (enableCache) {
515
Dispatcher.getCacheForType = getCacheForType;
516
Dispatcher.useCacheRefresh = useCacheRefresh;
517
}
518
+
519
+export let currentResponseState: null | ResponseState = (null: any);
520
+export function setCurrentResponseState(
521
+ responseState: null | ResponseState,
522
+): void {
523
+ currentResponseState = responseState;
524
+}
packages/react-server/src/ReactFizzServer.js
+5
-1
@@ -74,6 +74,8 @@ import {
74
finishHooks,
75
resetHooksState,
76
Dispatcher,
77
+ currentResponseState,
78
+ setCurrentResponseState,
79
} from './ReactFizzHooks';
80
81
import {
@@ -1341,7 +1343,8 @@ function performWork(request: Request): void {
1343
const prevContext = getActiveContext();
1344
const prevDispatcher = ReactCurrentDispatcher.current;
1345
ReactCurrentDispatcher.current = Dispatcher;
1344
-
1346
+ const prevResponseState = currentResponseState;
1347
+ setCurrentResponseState(request.responseState);
1348
try {
1349
const pingedTasks = request.pingedTasks;
1350
let i;
@@ -1357,6 +1360,7 @@ function performWork(request: Request): void {
1360
reportError(request, error);
1361
fatalError(request, error);
1362
} finally {
1363
+ setCurrentResponseState(prevResponseState);
1364
ReactCurrentDispatcher.current = prevDispatcher;
1365
if (prevDispatcher === Dispatcher) {
1366
// This means that we were in a reentrant work loop. This could happen
packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+2
@@ -28,11 +28,13 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
28
export opaque type ResponseState = mixed;
29
export opaque type FormatContext = mixed;
30
export opaque type SuspenseBoundaryID = mixed;
31
+export opaque type OpaqueIDType = mixed;
32
33
export const isPrimaryRenderer = false;
34
35
export const getChildFormatContext = $$$hostConfig.getChildFormatContext;
36
export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
37
+export const makeServerID = $$$hostConfig.makeServerID;
38
export const pushEmpty = $$$hostConfig.pushEmpty;
39
export const pushTextInstance = $$$hostConfig.pushTextInstance;
40
export const pushStartInstance = $$$hostConfig.pushStartInstance;
scripts/error-codes/codes.json
+2
-1
@@ -391,5 +391,6 @@
391
"400": "menuitems cannot have `children` nor `dangerouslySetInnerHTML`.",
392
"401": "The stacks must reach the root at the same time. This is a bug in React.",
393
"402": "The depth must equal at least at zero before reaching the root. This is a bug in React.",
394
- "403": "Tried to pop a Context at the root of the app. This is a bug in React."
394
+ "403": "Tried to pop a Context at the root of the app. This is a bug in React.",
395
+ "404": "Invalid hook call. Hooks can only be called inside of the body of a function component."
396
}