From dbced50a02935e075e87218d053a965088e0f586 Mon Sep 17 00:00:00 2001
From: Sebastian Markbage <sema@fb.com>
Date: Tue, 13 Apr 2021 23:20:05 -0400
Subject: [PATCH] Implement useOpaqueIdentifier

The format of this ID is specific to the format.
---
 .../src/server/ReactDOMServerFormatConfig.js  | 22 +++++++++++++++++--
 .../server/ReactNativeServerFormatConfig.js   | 15 +++++++++++++
 .../src/ReactNoopServer.js                    |  6 +++++
 packages/react-server/src/ReactFizzHooks.js   | 15 ++++++++++---
 packages/react-server/src/ReactFizzServer.js  |  6 ++++-
 .../forks/ReactServerFormatConfig.custom.js   |  2 ++
 scripts/error-codes/codes.json                |  3 ++-
 7 files changed, 62 insertions(+), 7 deletions(-)

diff --git a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
index 521fe04439c11..acc6e86879c81 100644
--- a/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom/src/server/ReactDOMServerFormatConfig.js
@@ -57,8 +57,9 @@ export type ResponseState = {
   placeholderPrefix: PrecomputedChunk,
   segmentPrefix: PrecomputedChunk,
   boundaryPrefix: string,
-  opaqueIdentifierPrefix: PrecomputedChunk,
+  opaqueIdentifierPrefix: string,
   nextSuspenseID: number,
+  nextOpaqueID: number,
   sentCompleteSegmentFunction: boolean,
   sentCompleteBoundaryFunction: boolean,
   sentClientRenderFunction: boolean,
@@ -72,8 +73,9 @@ export function createResponseState(
     placeholderPrefix: stringToPrecomputedChunk(identifierPrefix + 'P:'),
     segmentPrefix: stringToPrecomputedChunk(identifierPrefix + 'S:'),
     boundaryPrefix: identifierPrefix + 'B:',
-    opaqueIdentifierPrefix: stringToPrecomputedChunk(identifierPrefix + 'R:'),
+    opaqueIdentifierPrefix: identifierPrefix + 'R:',
     nextSuspenseID: 0,
+    nextOpaqueID: 0,
     sentCompleteSegmentFunction: false,
     sentCompleteBoundaryFunction: false,
     sentClientRenderFunction: false,
@@ -172,6 +174,22 @@ export function createSuspenseBoundaryID(
   return {formattedID: null};
 }
 
+export type OpaqueIDType = string;
+
+export function makeServerID(
+  responseState: null | ResponseState,
+): OpaqueIDType {
+  invariant(
+    responseState !== null,
+    'Invalid hook call. Hooks can only be called inside of the body of a function component.',
+  );
+  // TODO: This is not deterministic since it's created during render.
+  return (
+    responseState.opaqueIdentifierPrefix +
+    (responseState.nextOpaqueID++).toString(36)
+  );
+}
+
 function encodeHTMLTextNode(text: string): string {
   return escapeTextForBrowser(text);
 }
diff --git a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
index 223734c0483de..98779768f486c 100644
--- a/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
+++ b/packages/react-native-renderer/src/server/ReactNativeServerFormatConfig.js
@@ -61,12 +61,14 @@ SUSPENSE_UPDATE_TO_CLIENT_RENDER[0] = SUSPENSE_UPDATE_TO_CLIENT_RENDER_TAG;
 // Per response,
 export type ResponseState = {
   nextSuspenseID: number,
+  nextOpaqueID: number,
 };
 
 // Allows us to keep track of what we've already written so we can refer back to it.
 export function createResponseState(): ResponseState {
   return {
     nextSuspenseID: 0,
+    nextOpaqueID: 0,
   };
 }
 
@@ -108,6 +110,19 @@ export function createSuspenseBoundaryID(
   return responseState.nextSuspenseID++;
 }
 
+export type OpaqueIDType = number;
+
+export function makeServerID(
+  responseState: null | ResponseState,
+): OpaqueIDType {
+  invariant(
+    responseState !== null,
+    'Invalid hook call. Hooks can only be called inside of the body of a function component.',
+  );
+  // TODO: This is not deterministic since it's created during render.
+  return responseState.nextOpaqueID++;
+}
+
 const RAW_TEXT = stringToPrecomputedChunk('RCTRawText');
 
 export function pushEmpty(
diff --git a/packages/react-noop-renderer/src/ReactNoopServer.js b/packages/react-noop-renderer/src/ReactNoopServer.js
index 1f10b6aedd4f7..5a8ff56d6ce23 100644
--- a/packages/react-noop-renderer/src/ReactNoopServer.js
+++ b/packages/react-noop-renderer/src/ReactNoopServer.js
@@ -53,6 +53,8 @@ type Destination = {
 
 const POP = Buffer.from('/', 'utf8');
 
+let opaqueID = 0;
+
 const ReactNoopServer = ReactFizzServer({
   scheduleWork(callback: () => void) {
     callback();
@@ -84,6 +86,10 @@ const ReactNoopServer = ReactFizzServer({
     return {state: 'pending', children: []};
   },
 
+  makeServerID(): number {
+    return opaqueID++;
+  },
+
   getChildFormatContext(): null {
     return null;
   },
diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js
index 1650b5bf64c04..9fd9e0f420982 100644
--- a/packages/react-server/src/ReactFizzHooks.js
+++ b/packages/react-server/src/ReactFizzHooks.js
@@ -16,8 +16,12 @@ import type {
   ReactContext,
 } from 'shared/ReactTypes';
 
+import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
+
 import {readContext as readContextImpl} from './ReactFizzNewContext';
 
+import {makeServerID} from './ReactServerFormatConfig';
+
 import invariant from 'shared/invariant';
 import {enableCache} from 'shared/ReactFeatureFlags';
 import is from 'shared/objectIs';
@@ -41,8 +45,6 @@ type Hook = {|
   next: Hook | null,
 |};
 
-type OpaqueIDType = string;
-
 let currentlyRenderingComponent: Object | null = null;
 let firstWorkInProgressHook: Hook | null = null;
 let workInProgressHook: Hook | null = null;
@@ -474,7 +476,7 @@ function useTransition(): [(callback: () => void) => void, boolean] {
 }
 
 function useOpaqueIdentifier(): OpaqueIDType {
-  throw new Error('Not yet implemented.');
+  return makeServerID(currentResponseState);
 }
 
 function unsupportedRefresh() {
@@ -513,3 +515,10 @@ if (enableCache) {
   Dispatcher.getCacheForType = getCacheForType;
   Dispatcher.useCacheRefresh = useCacheRefresh;
 }
+
+export let currentResponseState: null | ResponseState = (null: any);
+export function setCurrentResponseState(
+  responseState: null | ResponseState,
+): void {
+  currentResponseState = responseState;
+}
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 4daaddbb32752..fb2b2e289b4d7 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -74,6 +74,8 @@ import {
   finishHooks,
   resetHooksState,
   Dispatcher,
+  currentResponseState,
+  setCurrentResponseState,
 } from './ReactFizzHooks';
 
 import {
@@ -1341,7 +1343,8 @@ function performWork(request: Request): void {
   const prevContext = getActiveContext();
   const prevDispatcher = ReactCurrentDispatcher.current;
   ReactCurrentDispatcher.current = Dispatcher;
-
+  const prevResponseState = currentResponseState;
+  setCurrentResponseState(request.responseState);
   try {
     const pingedTasks = request.pingedTasks;
     let i;
@@ -1357,6 +1360,7 @@ function performWork(request: Request): void {
     reportError(request, error);
     fatalError(request, error);
   } finally {
+    setCurrentResponseState(prevResponseState);
     ReactCurrentDispatcher.current = prevDispatcher;
     if (prevDispatcher === Dispatcher) {
       // This means that we were in a reentrant work loop. This could happen
diff --git a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
index 1ab6f95090822..7296697cc9c88 100644
--- a/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
+++ b/packages/react-server/src/forks/ReactServerFormatConfig.custom.js
@@ -28,11 +28,13 @@ export opaque type Destination = mixed; // eslint-disable-line no-undef
 export opaque type ResponseState = mixed;
 export opaque type FormatContext = mixed;
 export opaque type SuspenseBoundaryID = mixed;
+export opaque type OpaqueIDType = mixed;
 
 export const isPrimaryRenderer = false;
 
 export const getChildFormatContext = $$$hostConfig.getChildFormatContext;
 export const createSuspenseBoundaryID = $$$hostConfig.createSuspenseBoundaryID;
+export const makeServerID = $$$hostConfig.makeServerID;
 export const pushEmpty = $$$hostConfig.pushEmpty;
 export const pushTextInstance = $$$hostConfig.pushTextInstance;
 export const pushStartInstance = $$$hostConfig.pushStartInstance;
diff --git a/scripts/error-codes/codes.json b/scripts/error-codes/codes.json
index 4dbb95f55e809..538f28756e427 100644
--- a/scripts/error-codes/codes.json
+++ b/scripts/error-codes/codes.json
@@ -391,5 +391,6 @@
   "400": "menuitems cannot have `children` nor `dangerouslySetInnerHTML`.",
   "401": "The stacks must reach the root at the same time. This is a bug in React.",
   "402": "The depth must equal at least at zero before reaching the root. This is a bug in React.",
-  "403": "Tried to pop a Context at the root of the app. This is a bug in React."
+  "403": "Tried to pop a Context at the root of the app. This is a bug in React.",
+  "404": "Invalid hook call. Hooks can only be called inside of the body of a function component."
 }