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." }