Skip to content

Commit dd7ec17

Browse files
committed
Track owner/stack where the Flight Client reads as the root
This means that the owner of a Component rendered on the remote server becomes the Component on this server. Ideally we'd support this for the Client side too. In particular Fiber but currently ReactComponentInfo's owner is typed as only supporting other ReactComponentInfo and it's a bigger lift to support that.
1 parent 6066b8e commit dd7ec17

File tree

3 files changed

+123
-12
lines changed

3 files changed

+123
-12
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,19 @@ import isArray from 'shared/isArray';
8686

8787
import * as React from 'react';
8888

89+
import type {SharedStateServer} from 'react/src/ReactSharedInternalsServer';
90+
import type {SharedStateClient} from 'react/src/ReactSharedInternalsClient';
91+
8992
// TODO: This is an unfortunate hack. We shouldn't feature detect the internals
9093
// like this. It's just that for now we support the same build of the Flight
9194
// client both in the RSC environment, in the SSR environments as well as the
9295
// browser client. We should probably have a separate RSC build. This is DEV
9396
// only though.
94-
const ReactSharedInternals =
97+
const ReactSharedInteralsServer: void | SharedStateServer = (React: any)
98+
.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
99+
const ReactSharedInternals: SharedStateServer | SharedStateClient =
95100
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE ||
96-
React.__SERVER_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
101+
ReactSharedInteralsServer;
97102

98103
export type {CallServerCallback, EncodeFormActionCallback};
99104

@@ -277,6 +282,8 @@ export type Response = {
277282
_rowLength: number, // remaining bytes in the row. 0 indicates that we're looking for a newline.
278283
_buffer: Array<Uint8Array>, // chunks received so far as part of this row
279284
_tempRefs: void | TemporaryReferenceSet, // the set temporary references can be resolved from
285+
_debugRootOwner?: null | ReactComponentInfo, // DEV-only
286+
_debugRootStack?: null | Error, // DEV-only
280287
_debugRootTask?: null | ConsoleTask, // DEV-only
281288
_debugFindSourceMapURL?: void | FindSourceMapURLCallback, // DEV-only
282289
_replayConsole: boolean, // DEV-only
@@ -672,7 +679,7 @@ function createElement(
672679
type,
673680
key,
674681
props,
675-
_owner: owner,
682+
_owner: __DEV__ && owner === null ? response._debugRootOwner : owner,
676683
}: any);
677684
Object.defineProperty(element, 'ref', {
678685
enumerable: false,
@@ -699,7 +706,7 @@ function createElement(
699706
props,
700707

701708
// Record the component responsible for creating this element.
702-
_owner: owner,
709+
_owner: __DEV__ && owner === null ? response._debugRootOwner : owner,
703710
}: any);
704711
}
705712

@@ -733,7 +740,11 @@ function createElement(
733740
env = owner.env;
734741
}
735742
let normalizedStackTrace: null | Error = null;
736-
if (stack !== null) {
743+
if (owner === null && response._debugRootStack != null) {
744+
// We override the stack if we override the owner since the stack where the root JSX
745+
// was created on the server isn't very useful but where the request was made is.
746+
normalizedStackTrace = response._debugRootStack;
747+
} else if (stack !== null) {
737748
// We create a fake stack and then create an Error object inside of it.
738749
// This means that the stack trace is now normalized into the native format
739750
// of the browser and the stack frames will have been registered with
@@ -821,8 +832,10 @@ function createElement(
821832
if (enableOwnerStacks) {
822833
// $FlowFixMe[cannot-write]
823834
erroredComponent.debugStack = element._debugStack;
824-
// $FlowFixMe[cannot-write]
825-
erroredComponent.debugTask = element._debugTask;
835+
if (supportsCreateTask) {
836+
// $FlowFixMe[cannot-write]
837+
erroredComponent.debugTask = element._debugTask;
838+
}
826839
}
827840
erroredChunk._debugInfo = [erroredComponent];
828841
}
@@ -998,8 +1011,10 @@ function waitForReference<T>(
9981011
if (enableOwnerStacks) {
9991012
// $FlowFixMe[cannot-write]
10001013
erroredComponent.debugStack = element._debugStack;
1001-
// $FlowFixMe[cannot-write]
1002-
erroredComponent.debugTask = element._debugTask;
1014+
if (supportsCreateTask) {
1015+
// $FlowFixMe[cannot-write]
1016+
erroredComponent.debugTask = element._debugTask;
1017+
}
10031018
}
10041019
const chunkDebugInfo: ReactDebugInfo =
10051020
chunk._debugInfo || (chunk._debugInfo = []);
@@ -1408,6 +1423,25 @@ function ResponseInstance(
14081423
this._buffer = [];
14091424
this._tempRefs = temporaryReferences;
14101425
if (__DEV__) {
1426+
// TODO: The Flight Client can be used in a Client Environment too and we should really support
1427+
// getting the owner there as well, but currently the owner of ReactComponentInfo is typed as only
1428+
// supporting other ReactComponentInfo as owners (and not Fiber or Fizz's ComponentStackNode).
1429+
// We need to update all the callsites consuming ReactComponentInfo owners to support those.
1430+
// In the meantime we only check ReactSharedInteralsServer since we know that in an RSC environment
1431+
// the only owners will be ReactComponentInfo.
1432+
const rootOwner: null | ReactComponentInfo =
1433+
ReactSharedInteralsServer === undefined ||
1434+
ReactSharedInteralsServer.A === null
1435+
? null
1436+
: (ReactSharedInteralsServer.A.getOwner(): any);
1437+
1438+
this._debugRootOwner = rootOwner;
1439+
this._debugRootStack =
1440+
rootOwner !== null
1441+
? // TODO: Consider passing the top frame in so we can avoid internals showing up.
1442+
new Error('react-stack-top-frame')
1443+
: null;
1444+
14111445
const rootEnv = environmentName === undefined ? 'Server' : environmentName;
14121446
if (supportsCreateTask) {
14131447
// Any stacks that appear on the server need to be rooted somehow on the client
@@ -2308,7 +2342,16 @@ function resolveDebugInfo(
23082342
const env =
23092343
debugInfo.env === undefined ? response._rootEnvironmentName : debugInfo.env;
23102344
initializeFakeTask(response, debugInfo, env);
2311-
initializeFakeStack(response, debugInfo);
2345+
if (debugInfo.owner === null && response._debugRootOwner != null) {
2346+
// $FlowFixMe
2347+
debugInfo.owner = response._debugRootOwner;
2348+
// We override the stack if we override the owner since the stack where the root JSX
2349+
// was created on the server isn't very useful but where the request was made is.
2350+
// $FlowFixMe
2351+
debugInfo.debugStack = response._debugRootStack;
2352+
} else {
2353+
initializeFakeStack(response, debugInfo);
2354+
}
23122355

23132356
const chunk = getChunk(response, id);
23142357
const chunkDebugInfo: ReactDebugInfo =
@@ -2344,7 +2387,8 @@ const replayConsoleWithCallStack = {
23442387
// There really shouldn't be anything else on the stack atm.
23452388
const prevStack = ReactSharedInternals.getCurrentStack;
23462389
ReactSharedInternals.getCurrentStack = getCurrentStackInDEV;
2347-
currentOwnerInDEV = owner;
2390+
currentOwnerInDEV =
2391+
owner === null ? (response._debugRootOwner: any) : owner;
23482392

23492393
try {
23502394
const callStack = buildFakeCallStack(

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ function normalizeCodeLocInfo(str) {
2424
return (
2525
str &&
2626
str.replace(/^ +(?:at|in) ([\S]+)[^\n]*/gm, function (m, name) {
27+
let dot = name.indexOf('.');
28+
if (dot !== -1) {
29+
name = name.slice(dot + 1);
30+
}
2731
return ' in ' + name + (/\d/.test(m) ? ' (at **)' : '');
2832
})
2933
);
@@ -3124,6 +3128,69 @@ describe('ReactFlight', () => {
31243128
);
31253129
});
31263130

3131+
// @gate __DEV__ && enableOwnerStacks
3132+
it('can track owner for a flight response created in another render', async () => {
3133+
jest.resetModules();
3134+
jest.mock('react', () => ReactServer);
3135+
// For this to work the Flight Client needs to be the react-server version.
3136+
const ReactNoopFlightClienOnTheServer = require('react-noop-renderer/flight-client');
3137+
jest.resetModules();
3138+
jest.mock('react', () => React);
3139+
3140+
let stack;
3141+
3142+
function Component() {
3143+
stack = ReactServer.captureOwnerStack();
3144+
return ReactServer.createElement('span', null, 'hi');
3145+
}
3146+
3147+
const ClientComponent = clientReference(Component);
3148+
3149+
function ThirdPartyComponent() {
3150+
return ReactServer.createElement(ClientComponent);
3151+
}
3152+
3153+
// This is rendered outside the render to ensure we don't inherit anything accidental
3154+
// by being in the same environment which would make it seem like it works when it doesn't.
3155+
const thirdPartyTransport = ReactNoopFlightServer.render(
3156+
{children: ReactServer.createElement(ThirdPartyComponent)},
3157+
{
3158+
environmentName: 'third-party',
3159+
},
3160+
);
3161+
3162+
async function fetchThirdParty() {
3163+
return ReactNoopFlightClienOnTheServer.read(thirdPartyTransport);
3164+
}
3165+
3166+
async function FirstPartyComponent() {
3167+
// This component fetches from a third party
3168+
const thirdParty = await fetchThirdParty();
3169+
return thirdParty.children;
3170+
}
3171+
function App() {
3172+
return ReactServer.createElement(FirstPartyComponent);
3173+
}
3174+
3175+
const transport = ReactNoopFlightServer.render(
3176+
ReactServer.createElement(App),
3177+
);
3178+
3179+
await act(async () => {
3180+
const root = await ReactNoopFlightClient.read(transport);
3181+
ReactNoop.render(root);
3182+
});
3183+
3184+
expect(normalizeCodeLocInfo(stack)).toBe(
3185+
'\n in ThirdPartyComponent (at **)' +
3186+
'\n in createResponse (at **)' + // These two internal frames should
3187+
'\n in read (at **)' + // ideally not be included.
3188+
'\n in fetchThirdParty (at **)' +
3189+
'\n in FirstPartyComponent (at **)' +
3190+
'\n in App (at **)',
3191+
);
3192+
});
3193+
31273194
// @gate __DEV__ && enableOwnerStacks
31283195
it('can get the component owner stacks for onError in dev', async () => {
31293196
const thrownError = new Error('hi');

packages/react-server/src/ReactFlightServer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2271,7 +2271,7 @@ function isReactComponentInfo(value: any): boolean {
22712271
typeof value.debugTask.run === 'function') ||
22722272
value.debugStack instanceof Error) &&
22732273
(enableOwnerStacks
2274-
? isArray((value: any).stack)
2274+
? isArray((value: any).stack) || (value: any).stack === null
22752275
: typeof (value: any).stack === 'undefined') &&
22762276
typeof value.name === 'string' &&
22772277
typeof value.env === 'string' &&

0 commit comments

Comments
 (0)