Skip to content

Commit a0c5e47

Browse files
committed
Track owner on componentStorage
1 parent e1539bc commit a0c5e47

File tree

4 files changed

+111
-16
lines changed

4 files changed

+111
-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();
@@ -456,4 +460,71 @@ describe('ReactFlightDOMEdge', () => {
456460
{withoutStack: true},
457461
);
458462
});
463+
464+
it('supports async server component debug info as the element owner in DEV', async () => {
465+
function Container({children}) {
466+
return children;
467+
}
468+
469+
const promise = Promise.resolve(true);
470+
async function Greeting({firstName}) {
471+
// We can't use JSX here because it'll use the Client React.
472+
const child = ReactServer.createElement(
473+
'span',
474+
null,
475+
'Hello, ' + firstName,
476+
);
477+
// Yield the synchronous pass
478+
await promise;
479+
// We should still be able to track owner using AsyncLocalStorage.
480+
return ReactServer.createElement(Container, null, child);
481+
}
482+
483+
const model = {
484+
greeting: ReactServer.createElement(Greeting, {firstName: 'Seb'}),
485+
};
486+
487+
const stream = ReactServerDOMServer.renderToReadableStream(
488+
model,
489+
webpackMap,
490+
);
491+
492+
const rootModel = await ReactServerDOMClient.createFromReadableStream(
493+
stream,
494+
{
495+
ssrManifest: {
496+
moduleMap: null,
497+
moduleLoading: null,
498+
},
499+
},
500+
);
501+
502+
const ssrStream = await ReactDOMServer.renderToReadableStream(
503+
rootModel.greeting,
504+
);
505+
const result = await readResult(ssrStream);
506+
expect(result).toEqual('<span>Hello, Seb</span>');
507+
508+
// Resolve the React Lazy wrapper which must have resolved by now.
509+
const lazyWrapper = rootModel.greeting;
510+
const greeting = lazyWrapper._init(lazyWrapper._payload);
511+
512+
// We've rendered down to the span.
513+
expect(greeting.type).toBe('span');
514+
if (__DEV__) {
515+
const greetInfo = {name: 'Greeting', env: 'Server', owner: null};
516+
expect(lazyWrapper._debugInfo).toEqual([
517+
greetInfo,
518+
{name: 'Container', env: 'Server', owner: greetInfo},
519+
]);
520+
// The owner that created the span was the outer server component.
521+
// We expect the debug info to be referentially equal to the owner.
522+
expect(greeting._owner).toBe(lazyWrapper._debugInfo[0]);
523+
} else {
524+
expect(lazyWrapper._debugInfo).toBe(undefined);
525+
expect(greeting._owner).toBe(
526+
gate(flags => flags.disableStringRefs) ? undefined : null,
527+
);
528+
}
529+
});
459530
});

packages/react-server/src/ReactFlightServer.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ import {
7171
isServerReference,
7272
supportsRequestStorage,
7373
requestStorage,
74+
supportsComponentStorage,
75+
componentStorage,
7476
createHints,
7577
initAsyncDebugInfo,
7678
} from './ReactFlightServerConfig';
@@ -89,7 +91,7 @@ import {
8991
} from './ReactFlightHooks';
9092
import {DefaultAsyncDispatcher} from './flight/ReactFlightAsyncDispatcher';
9193

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

9496
import {
9597
getIteratorFn,
@@ -157,7 +159,7 @@ function patchConsole(consoleInst: typeof console, methodName: string) {
157159
// We don't currently use this id for anything but we emit it so that we can later
158160
// refer to previous logs in debug info to associate them with a component.
159161
const id = request.nextChunkId++;
160-
const owner: null | ReactComponentInfo = currentOwner;
162+
const owner: null | ReactComponentInfo = resolveOwner();
161163
emitConsoleChunk(request, id, methodName, owner, stack, arguments);
162164
}
163165
// $FlowFixMe[prop-missing]
@@ -608,7 +610,11 @@ function renderFunctionComponent<Props>(
608610
const prevThenableState = task.thenableState;
609611
task.thenableState = null;
610612

611-
let componentDebugInfo: null | ReactComponentInfo = null;
613+
// The secondArg is always undefined in Server Components since refs error early.
614+
const secondArg = undefined;
615+
let result;
616+
617+
let componentDebugInfo: ReactComponentInfo;
612618
if (__DEV__) {
613619
if (debugID === null) {
614620
// We don't have a chunk to assign debug info. We need to outline this
@@ -637,22 +643,28 @@ function renderFunctionComponent<Props>(
637643
outlineModel(request, componentDebugInfo);
638644
emitDebugChunk(request, componentDebugID, componentDebugInfo);
639645
}
640-
}
641-
642-
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
643-
// The secondArg is always undefined in Server Components since refs error early.
644-
const secondArg = undefined;
645-
let result;
646-
if (__DEV__) {
646+
prepareToUseHooksForComponent(prevThenableState, componentDebugInfo);
647647
setCurrentOwner(componentDebugInfo);
648648
try {
649-
result = Component(props, secondArg);
649+
if (supportsComponentStorage) {
650+
// Run the component in an Async Context that tracks the current owner.
651+
result = componentStorage.run(
652+
componentDebugInfo,
653+
Component,
654+
props,
655+
secondArg,
656+
);
657+
} else {
658+
result = Component(props, secondArg);
659+
}
650660
} finally {
651661
setCurrentOwner(null);
652662
}
653663
} else {
664+
prepareToUseHooksForComponent(prevThenableState, null);
654665
result = Component(props, secondArg);
655666
}
667+
656668
if (
657669
typeof result === 'object' &&
658670
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)