[Flight] Let environmentName vary over time by making it a function of string (#29867)
This lets the environment name vary within a request by the context a component, log or error being executed in. A potentially different API would be something like `setEnvironmentName()` but we'd have to extend the `ReadableStream` or something to do that like we do for `.allReady`. As a function though it has some expansion possibilities, e.g. we could potentially also pass some information to it for context about what is being asked for. If it changes before completing a task, we also emit the change so that we have the debug info for what the environment was before entering a component and what it was after completing it.
Sebastian Markbåge committed
Jun 12, 2024 at 10:55 UTC
55c9d45f3b9239d5997788f10b8cdad5bcb81daf
10 files changed
+92
-18
packages/react-client/src/__tests__/ReactFlight-test.js
+44
@@ -2565,6 +2565,50 @@ describe('ReactFlight', () => {
2565
);
2566
});
2567
2568
+ it('can change the environment name inside a component', async () => {
2569
+ let env = 'A';
2570
+ function Component(props) {
2571
+ env = 'B';
2572
+ return <div>hi</div>;
2573
+ }
2574
+
2575
+ const transport = ReactNoopFlightServer.render(
2576
+ {
2577
+ greeting: <Component />,
2578
+ },
2579
+ {
2580
+ environmentName() {
2581
+ return env;
2582
+ },
2583
+ },
2584
+ );
2585
+
2586
+ await act(async () => {
2587
+ const rootModel = await ReactNoopFlightClient.read(transport);
2588
+ const greeting = rootModel.greeting;
2589
+ expect(getDebugInfo(greeting)).toEqual(
2590
+ __DEV__
2591
+ ? [
2592
+ {
2593
+ name: 'Component',
2594
+ env: 'A',
2595
+ owner: null,
2596
+ stack: gate(flag => flag.enableOwnerStacks)
2597
+ ? ' in Object.<anonymous> (at **)'
2598
+ : undefined,
2599
+ },
2600
+ {
2601
+ env: 'B',
2602
+ },
2603
+ ]
2604
+ : undefined,
2605
+ );
2606
+ ReactNoop.render(greeting);
2607
+ });
2608
+
2609
+ expect(ReactNoop).toMatchRenderedOutput(<div>hi</div>);
2610
+ });
2611
+
2612
// @gate enableServerComponentLogs && __DEV__
2613
it('replays logs, but not onError logs', async () => {
2614
function foo() {
packages/react-noop-renderer/src/ReactNoopFlightServer.js
+1
-1
@@ -67,7 +67,7 @@ const ReactNoopFlightServer = ReactFlightServer({
67
});
68
69
type Options = {
70
- environmentName?: string,
70
+ environmentName?: string | (() => string),
71
identifierPrefix?: string,
72
onError?: (error: mixed) => void,
73
onPostpone?: (reason: string) => void,
packages/react-server-dom-esm/src/ReactFlightDOMServerNode.js
+1
-1
@@ -66,7 +66,7 @@ function createCancelHandler(request: Request, reason: string) {
66
}
67
68
type Options = {
69
- environmentName?: string,
69
+ environmentName?: string | (() => string),
70
onError?: (error: mixed) => void,
71
onPostpone?: (reason: string) => void,
72
identifierPrefix?: string,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerBrowser.js
+1
-1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44
export type {TemporaryReferenceSet};
45
46
type Options = {
47
- environmentName?: string,
47
+ environmentName?: string | (() => string),
48
identifierPrefix?: string,
49
signal?: AbortSignal,
50
temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerEdge.js
+1
-1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44
export type {TemporaryReferenceSet};
45
46
type Options = {
47
- environmentName?: string,
47
+ environmentName?: string | (() => string),
48
identifierPrefix?: string,
49
signal?: AbortSignal,
50
temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-turbopack/src/ReactFlightDOMServerNode.js
+1
-1
@@ -67,7 +67,7 @@ function createCancelHandler(request: Request, reason: string) {
67
}
68
69
type Options = {
70
- environmentName?: string,
70
+ environmentName?: string | (() => string),
71
onError?: (error: mixed) => void,
72
onPostpone?: (reason: string) => void,
73
identifierPrefix?: string,
packages/react-server-dom-webpack/src/ReactFlightDOMServerBrowser.js
+1
-1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44
export type {TemporaryReferenceSet};
45
46
type Options = {
47
- environmentName?: string,
47
+ environmentName?: string | (() => string),
48
identifierPrefix?: string,
49
signal?: AbortSignal,
50
temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-webpack/src/ReactFlightDOMServerEdge.js
+1
-1
@@ -44,7 +44,7 @@ export {createTemporaryReferenceSet} from 'react-server/src/ReactFlightServerTem
44
export type {TemporaryReferenceSet};
45
46
type Options = {
47
- environmentName?: string,
47
+ environmentName?: string | (() => string),
48
identifierPrefix?: string,
49
signal?: AbortSignal,
50
temporaryReferences?: TemporaryReferenceSet,
packages/react-server-dom-webpack/src/ReactFlightDOMServerNode.js
+1
-1
@@ -67,7 +67,7 @@ function createCancelHandler(request: Request, reason: string) {
67
}
68
69
type Options = {
70
- environmentName?: string,
70
+ environmentName?: string | (() => string),
71
onError?: (error: mixed) => void,
72
onPostpone?: (reason: string) => void,
73
identifierPrefix?: string,
packages/react-server/src/ReactFlightServer.js
+40
-10
@@ -393,6 +393,7 @@ type Task = {
393
keyPath: null | string, // parent server component keys
394
implicitSlot: boolean, // true if the root server component of this sequence had a null key
395
thenableState: ThenableState | null,
396
+ environmentName: string, // DEV-only. Used to track if the environment for this task changed.
397
};
398
399
interface Reference {}
@@ -425,7 +426,7 @@ export type Request = {
426
onError: (error: mixed) => ?string,
427
onPostpone: (reason: string) => void,
428
// DEV-only
428
- environmentName: string,
429
+ environmentName: () => string,
430
didWarnForKey: null | WeakSet<ReactComponentInfo>,
431
};
432
@@ -481,7 +482,7 @@ function RequestInstance(
482
onError: void | ((error: mixed) => ?string),
483
identifierPrefix?: string,
484
onPostpone: void | ((reason: string) => void),
484
- environmentName: void | string,
485
+ environmentName: void | string | (() => string),
486
temporaryReferences: void | TemporaryReferenceSet,
487
) {
488
if (
@@ -531,7 +532,11 @@ function RequestInstance(
532
533
if (__DEV__) {
534
this.environmentName =
534
- environmentName === undefined ? 'Server' : environmentName;
535
+ environmentName === undefined
536
+ ? () => 'Server'
537
+ : typeof environmentName !== 'function'
538
+ ? () => environmentName
539
+ : environmentName;
540
this.didWarnForKey = null;
541
}
542
const rootTask = createTask(this, model, null, false, abortSet);
@@ -544,7 +549,7 @@ export function createRequest(
549
onError: void | ((error: mixed) => ?string),
550
identifierPrefix?: string,
551
onPostpone: void | ((reason: string) => void),
547
- environmentName: void | string,
552
+ environmentName: void | string | (() => string),
553
temporaryReferences: void | TemporaryReferenceSet,
554
): Request {
555
// $FlowFixMe[invalid-constructor]: the shapes are exact here but Flow doesn't like constructors
@@ -1049,14 +1054,14 @@ function renderFunctionComponent<Props>(
1054
componentDebugInfo = (prevThenableState: any)._componentDebugInfo;
1055
} else {
1056
// This is a new component in the same task so we can emit more debug info.
1057
+ const componentDebugID = debugID;
1058
const componentName =
1059
(Component: any).displayName || Component.name || '';
1060
+ const componentEnv = request.environmentName();
1061
request.pendingChunks++;
1055
-
1056
- const componentDebugID = debugID;
1062
componentDebugInfo = ({
1063
name: componentName,
1059
- env: request.environmentName,
1064
+ env: componentEnv,
1065
owner: owner,
1066
}: ReactComponentInfo);
1067
if (enableOwnerStacks) {
@@ -1069,6 +1074,9 @@ function renderFunctionComponent<Props>(
1074
outlineModel(request, componentDebugInfo);
1075
emitDebugChunk(request, componentDebugID, componentDebugInfo);
1076
1077
+ // We've emitted the latest environment for this task so we track that.
1078
+ task.environmentName = componentEnv;
1079
+
1080
if (enableOwnerStacks) {
1081
warnForMissingKey(request, key, validated, componentDebugInfo);
1082
}
@@ -1644,7 +1652,7 @@ function createTask(
1652
request.writtenObjects.set(model, serializeByValueID(id));
1653
}
1654
}
1647
- const task: Task = {
1655
+ const task: Task = (({
1656
id,
1657
status: PENDING,
1658
model,
@@ -1697,7 +1705,10 @@ function createTask(
1705
return renderModel(request, task, parent, parentPropertyName, value);
1706
},
1707
thenableState: null,
1700
- };
1708
+ }: Omit<Task, 'environmentName'>): any);
1709
+ if (__DEV__) {
1710
+ task.environmentName = request.environmentName();
1711
+ }
1712
abortSet.add(task);
1713
return task;
1714
}
@@ -3252,7 +3263,7 @@ function emitConsoleChunk(
3263
}
3264
3265
// TODO: Don't double badge if this log came from another Flight Client.
3255
- const env = request.environmentName;
3266
+ const env = request.environmentName();
3267
const payload = [methodName, stackTrace, owner, env];
3268
// $FlowFixMe[method-unbinding]
3269
payload.push.apply(payload, args);
@@ -3420,6 +3431,15 @@ function retryTask(request: Request, task: Task): void {
3431
// any future references.
3432
request.writtenObjects.set(resolvedModel, serializeByValueID(task.id));
3433
3434
+ if (__DEV__) {
3435
+ const currentEnv = request.environmentName();
3436
+ if (currentEnv !== task.environmentName) {
3437
+ // The environment changed since we last emitted any debug information for this
3438
+ // task. We emit an entry that just includes the environment name change.
3439
+ emitDebugChunk(request, task.id, {env: currentEnv});
3440
+ }
3441
+ }
3442
+
3443
// Object might contain unresolved values like additional elements.
3444
// This is simulating what the JSON loop would do if this was part of it.
3445
emitChunk(request, task, resolvedModel);
@@ -3428,6 +3448,16 @@ function retryTask(request: Request, task: Task): void {
3448
// We don't need to escape it again so it's not passed the toJSON replacer.
3449
// $FlowFixMe[incompatible-type] stringify can return null for undefined but we never do
3450
const json: string = stringify(resolvedModel);
3451
+
3452
+ if (__DEV__) {
3453
+ const currentEnv = request.environmentName();
3454
+ if (currentEnv !== task.environmentName) {
3455
+ // The environment changed since we last emitted any debug information for this
3456
+ // task. We emit an entry that just includes the environment name change.
3457
+ emitDebugChunk(request, task.id, {env: currentEnv});
3458
+ }
3459
+ }
3460
+
3461
emitModelChunk(request, task.id, json);
3462
}
3463