Files
react/packages/react-server/src/ReactFlightHooks.js
T
Sebastian Markbåge f33a6b69c6 Track Owner for Server Components in DEV (#28753)
This implements the concept of a DEV-only "owner" for Server Components.
The owner concept isn't really super useful. We barely use it anymore,
but we do have it as a concept in DevTools in a couple of cases so this
adds it for parity. However, this is mainly interesting because it could
be used to wire up future owner-based stacks.

I do this by outlining the DebugInfo for a Server Component
(ReactComponentInfo). Then I just rely on Flight deduping to refer to
that. I refer to the same thing by referential equality so that we can
associate a Server Component parent in DebugInfo with an owner.

If you suspend and replay a Server Component, we have to restore the
same owner. To do that, I did a little ugly hack and stashed it on the
thenable state object. Felt unnecessarily complicated to add a stateful
wrapper for this one dev-only case.

The owner could really be anything since it could be coming from a
different implementation. Because this is the first time we have an
owner other than Fiber, I have to fix up a bunch of places that assumes
Fiber. I mainly did the `typeof owner.tag === 'number'` to assume it's a
Fiber for now.

This also doesn't actually add it to DevTools / RN Inspector yet. I just
ignore them there for now.

Because Server Components can be async the owner isn't tracked after an
await. We need per-component AsyncLocalStorage for that. This can be
done in a follow up.
2024-04-05 12:48:52 -04:00

154 lines
4.8 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {Request} from './ReactFlightServer';
import type {Thenable, Usable, ReactComponentInfo} from 'shared/ReactTypes';
import type {ThenableState} from './ReactFlightThenable';
import {
REACT_MEMO_CACHE_SENTINEL,
REACT_CONTEXT_TYPE,
} from 'shared/ReactSymbols';
import {createThenableState, trackUsedThenable} from './ReactFlightThenable';
import {isClientReference} from './ReactFlightServerConfig';
let currentRequest = null;
let thenableIndexCounter = 0;
let thenableState = null;
let currentComponentDebugInfo = null;
export function prepareToUseHooksForRequest(request: Request) {
currentRequest = request;
}
export function resetHooksForRequest() {
currentRequest = null;
}
export function prepareToUseHooksForComponent(
prevThenableState: ThenableState | null,
componentDebugInfo: null | ReactComponentInfo,
) {
thenableIndexCounter = 0;
thenableState = prevThenableState;
if (__DEV__) {
currentComponentDebugInfo = componentDebugInfo;
}
}
export function getThenableStateAfterSuspending(): ThenableState {
// If you use() to Suspend this should always exist but if you throw a Promise instead,
// which is not really supported anymore, it will be empty. We use the empty set as a
// marker to know if this was a replay of the same component or first attempt.
const state = thenableState || createThenableState();
if (__DEV__) {
// This is a hack but we stash the debug info here so that we don't need a completely
// different data structure just for this in DEV. Not too happy about it.
(state: any)._componentDebugInfo = currentComponentDebugInfo;
currentComponentDebugInfo = null;
}
thenableState = null;
return state;
}
export const HooksDispatcher: Dispatcher = {
useMemo<T>(nextCreate: () => T): T {
return nextCreate();
},
useCallback<T>(callback: T): T {
return callback;
},
useDebugValue(): void {},
useDeferredValue: (unsupportedHook: any),
useTransition: (unsupportedHook: any),
readContext: (unsupportedContext: any),
useContext: (unsupportedContext: any),
useReducer: (unsupportedHook: any),
useRef: (unsupportedHook: any),
useState: (unsupportedHook: any),
useInsertionEffect: (unsupportedHook: any),
useLayoutEffect: (unsupportedHook: any),
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
useId,
useSyncExternalStore: (unsupportedHook: any),
useCacheRefresh(): <T>(?() => T, ?T) => void {
return unsupportedRefresh;
},
useMemoCache(size: number): Array<any> {
const data = new Array<any>(size);
for (let i = 0; i < size; i++) {
data[i] = REACT_MEMO_CACHE_SENTINEL;
}
return data;
},
use,
};
function unsupportedHook(): void {
throw new Error('This Hook is not supported in Server Components.');
}
function unsupportedRefresh(): void {
throw new Error(
'Refreshing the cache is not supported in Server Components.',
);
}
function unsupportedContext(): void {
throw new Error('Cannot read a Client Context from a Server Component.');
}
function useId(): string {
if (currentRequest === null) {
throw new Error('useId can only be used while React is rendering');
}
const id = currentRequest.identifierCount++;
// use 'S' for Flight components to distinguish from 'R' and 'r' in Fizz/Client
return ':' + currentRequest.identifierPrefix + 'S' + id.toString(32) + ':';
}
function use<T>(usable: Usable<T>): T {
if (
(usable !== null && typeof usable === 'object') ||
typeof usable === 'function'
) {
// $FlowFixMe[method-unbinding]
if (typeof usable.then === 'function') {
// This is a thenable.
const thenable: Thenable<T> = (usable: any);
// Track the position of the thenable within this fiber.
const index = thenableIndexCounter;
thenableIndexCounter += 1;
if (thenableState === null) {
thenableState = createThenableState();
}
return trackUsedThenable(thenableState, thenable, index);
} else if (usable.$$typeof === REACT_CONTEXT_TYPE) {
unsupportedContext();
}
}
if (isClientReference(usable)) {
if (usable.value != null && usable.value.$$typeof === REACT_CONTEXT_TYPE) {
// Show a more specific message since it's a common mistake.
throw new Error('Cannot read a Client Context from a Server Component.');
} else {
throw new Error('Cannot use() an already resolved Client Reference.');
}
} else {
throw new Error(
// eslint-disable-next-line react-internal/safe-string-coercion
'An unsupported type was passed to use(): ' + String(usable),
);
}
}