Skip to content

Commit a2d2524

Browse files
committed
Track owner on componentStorage
1 parent df153c9 commit a2d2524

File tree

4 files changed

+110
-16
lines changed

4 files changed

+110
-16
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMEdge-test.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ if (typeof Blob === 'undefined') {
2121
if (typeof File === 'undefined') {
2222
global.File = require('buffer').File;
2323
}
24+
// Patch for Edge environments for global scope
25+
global.AsyncLocalStorage = require('async_hooks').AsyncLocalStorage;
2426

2527
// Don't wait before processing work on the server.
2628
// TODO: we can replace this with FlightServer.act().
@@ -32,6 +34,7 @@ let webpackMap;
3234
let webpackModules;
3335
let webpackModuleLoading;
3436
let React;
37+
let ReactServer;
3538
let ReactDOMServer;
3639
let ReactServerDOMServer;
3740
let ReactServerDOMClient;
@@ -55,6 +58,7 @@ describe('ReactFlightDOMEdge', () => {
5558
webpackModules = WebpackMock.webpackModules;
5659
webpackModuleLoading = WebpackMock.moduleLoading;
5760

61+
ReactServer = require('react');
5862
ReactServerDOMServer = require('react-server-dom-webpack/server');
5963

6064
jest.resetModules();
@@ -692,4 +696,71 @@ describe('ReactFlightDOMEdge', () => {
692696
),
693697
);
694698
});
699+
700+
it('supports async server component debug info as the element owner in DEV', async () => {
701+
function Container({children}) {
702+
return children;
703+
}
704+
705+
const promise = Promise.resolve(true);
706+
async function Greeting({firstName}) {
707+
// We can't use JSX here because it'll use the Client React.
708+
const child = ReactServer.createElement(
709+
'span',
710+
null,
711+
'Hello, ' + firstName,
712+
);
713+
// Yield the synchronous pass
714+
await promise;
715+
// We should still be able to track owner using AsyncLocalStorage.
716+
return ReactServer.createElement(Container, null, child);
717+
}
718+
719+
const model = {
720+
greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
721+
};
722+
723+
const stream = ReactServerDOMServer.renderToReadableStream(
724+
model,
725+
webpackMap,
726+
);
727+
728+
const rootModel = await ReactServerDOMClient.createFromReadableStream(
729+
stream,
730+
{
731+
ssrManifest: {
732+
moduleMap: null,
733+
moduleLoading: null,
734+
},
735+
},
736+
);
737+
738+
const ssrStream = await ReactDOMServer.renderToReadableStream(
739+
rootModel.greeting,
740+
);
741+
const result = await readResult(ssrStream);
742+
expect(result).toEqual('<span>Hello, Seb</span>');
743+
744+
// Resolve the React Lazy wrapper which must have resolved by now.
745+
const lazyWrapper = rootModel.greeting;
746+
const greeting = lazyWrapper._init(lazyWrapper._payload);
747+
748+
// We've rendered down to the span.
749+
expect(greeting.type).toBe('span');
750+
if (__DEV__) {
751+
const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
752+
expect(lazyWrapper._debugInfo).toEqual([
753+
greetInfo,
754+
{name: 'Container', env: 'Server', owner: greetInfo},
755+
]);
756+
// The owner that created the span was the outer server component.
757+
// We expect the debug info to be referentially equal to the owner.
758+
expect(greeting._owner).toBe(lazyWrapper._debugInfo[0]);
759+
} else {
760+
expect(lazyWrapper._debugInfo).toBe(undefined);
761+
expect(greeting._owner).toBe(
762+
gate(flags => flags.disableStringRefs) ? undefined : null,
763+
);
764+
}
765+
});
695766
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ import {
7373
isServerReference,
7474
supportsRequestStorage,
7575
requestStorage,
76+
supportsComponentStorage,
77+
componentStorage,
7678
createHints,
7779
initAsyncDebugInfo,
7880
} from './ReactFlightServerConfig';
@@ -91,7 +93,7 @@ import {
9193
} from './ReactFlightHooks';
9294
import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher';
9395

94-
import {currentOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner';
96+
import {resolveOwner, setCurrentOwner} from './flight/ReactFlightCurrentOwner';
9597

9698
import {
9799
getIteratorFn,
@@ -160,7 +162,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
160162
// We don't currently use this id for anything but we emit it so that we can later
161163
// refer to previous logs in debug info to associate them with a component.
162164
const id = request.nextChunkId++;
163-
const owner: null | ReactComponentInfo = currentOwner;
165+
const owner: null | ReactComponentInfo = resolveOwner();
164166
emitConsoleChunk(request, id, methodName, owner, stack, arguments);
165167
}
166168
// $FlowFixMe[prop-missing]
@@ -822,7 +824,11 @@ function renderFunctionComponent<Props>(
822824
const prevThenableState = task.thenableState;
823825
task.thenableState = null;
824826

825-
let componentDebugInfo: null | ReactComponentInfo = null;
827+
// The secondArg is always undefined in Server Components since refs error early.
828+
const secondArg = undefined;
829+
let result;
830+
831+
let componentDebugInfo: ReactComponentInfo;
826832
if (__DEV__) {
827833
if (debugID === null) {
828834
// We don't have a chunk to assign debug info. We need to outline this
@@ -851,20 +857,25 @@ function renderFunctionComponent<Props>(
851857
outlineModel(request, componentDebugInfo);
852858
emitDebugChunk(request, componentDebugID, componentDebugInfo);
853859
}
854-
}
855-
856-
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
857-
// The secondArg is always undefined in Server Components since refs error early.
858-
const secondArg = undefined;
859-
let result;
860-
if (__DEV__) {
860+
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
861861
setCurrentOwner(componentDebugInfo);
862862
try {
863-
result = Component(props, secondArg);
863+
if (supportsComponentStorage) {
864+
// Run the component in an Async Context that tracks the current owner.
865+
result = componentStorage.run(
866+
componentDebugInfo,
867+
Component,
868+
props,
869+
secondArg,
870+
);
871+
} else {
872+
result = Component(props, secondArg);
873+
}
864874
} finally {
865875
setCurrentOwner(null);
866876
}
867877
} else {
878+
prepareToUseHooksForComponent(prevThenableState, null);
868879
result = Component(props, secondArg);
869880
}
870881
if (typeof result === 'object' && result !== null) {

packages/react-server/src/flight/ReactFlightAsyncDispatcher.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {resolveRequest, getCache} from '../ReactFlightServer';
1515

1616
import {disableStringRefs} from 'shared/ReactFeatureFlags';
1717

18-
import {currentOwner} from './ReactFlightCurrentOwner';
18+
import {resolveOwner} from './ReactFlightCurrentOwner';
1919

2020
function resolveCache(): Map<Function, mixed> {
2121
const request = resolveRequest();
@@ -39,9 +39,7 @@ export const DefaultAsyncDispatcher: AsyncDispatcher = ({
3939
}: any);
4040

4141
if (__DEV__) {
42-
DefaultAsyncDispatcher.getOwner = (): null | ReactComponentInfo => {
43-
return currentOwner;
44-
};
42+
DefaultAsyncDispatcher.getOwner = resolveOwner;
4543
} else if (!disableStringRefs) {
4644
// Server Components never use string refs but the JSX runtime looks for it.
4745
DefaultAsyncDispatcher.getOwner = (): null | ReactComponentInfo => {

packages/react-server/src/flight/ReactFlightCurrentOwner.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,22 @@
99

1010
import type {ReactComponentInfo} from 'shared/ReactTypes';
1111

12-
export let currentOwner: ReactComponentInfo | null = null;
12+
import {
13+
supportsComponentStorage,
14+
componentStorage,
15+
} from '../ReactFlightServerConfig';
16+
17+
let currentOwner: ReactComponentInfo | null = null;
1318

1419
export function setCurrentOwner(componentInfo: null | ReactComponentInfo) {
1520
currentOwner = componentInfo;
1621
}
22+
23+
export function resolveOwner(): null | ReactComponentInfo {
24+
if (currentOwner) return currentOwner;
25+
if (supportsComponentStorage) {
26+
const owner = componentStorage.getStore();
27+
if (owner) return owner;
28+
}
29+
return null;
30+
}

0 commit comments

Comments
 (0)