Skip to content

Commit b0c1dc0

Browse files
authored
[Flight] Add approximate parent context for FormatContext (facebook#34601)
Flight doesn't have any semantically sound notion of a parent context. That's why we removed Server Context. Each root can really start anywhere in the tree when you refetch subtrees. Additionally when you dedupe elements they can end up in multiple different parent contexts. However, we do have a DEV only version of this with debugTask being tracked for the nearest parent element to track the context of properties inside of it. To apply certain DOM specific hints and optimizations when you render host components we need some information of the context. This is usually very local so doesn't suffer from the likelihood that you refetch in the middle. We'll also only use this information for optimistic hints and not hard semantics so getting it wrong isn't terrible. ``` <picture> <img /> </picture> <noscript> <p> <img /> </p> </noscript> ``` For example, in these cases we should exclude preloading the image but we have to know if that's the scope we're in. We can easily get this wrong if they're split or even if they're wrapped in client components that we don't know about like: ``` <NoScript> <p> <img /> </p> </NoScript> ``` However, getting it wrong in either direction is not the end of the world. It's about covering the common cases well.
1 parent 6eb5d67 commit b0c1dc0

File tree

5 files changed

+101
-0
lines changed

5 files changed

+101
-0
lines changed

packages/react-dom-bindings/src/server/ReactFlightServerConfigDOM.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,17 @@ export type Hints = Set<string>;
6161
export function createHints(): Hints {
6262
return new Set();
6363
}
64+
65+
export opaque type FormatContext = null;
66+
67+
export function createRootFormatContext(): FormatContext {
68+
return null;
69+
}
70+
71+
export function getChildFormatContext(
72+
parentContext: FormatContext,
73+
type: string,
74+
props: Object,
75+
): FormatContext {
76+
return parentContext;
77+
}

packages/react-server/src/ReactFlightServer.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import type {
4949
Hints,
5050
HintCode,
5151
HintModel,
52+
FormatContext,
5253
} from './ReactFlightServerConfig';
5354
import type {ThenableState} from './ReactFlightThenable';
5455
import type {
@@ -88,6 +89,8 @@ import {
8889
supportsRequestStorage,
8990
requestStorage,
9091
createHints,
92+
createRootFormatContext,
93+
getChildFormatContext,
9194
initAsyncDebugInfo,
9295
markAsyncSequenceRootTask,
9396
getCurrentAsyncSequence,
@@ -525,6 +528,7 @@ type Task = {
525528
toJSON: (key: string, value: ReactClientValue) => ReactJSONValue,
526529
keyPath: null | string, // parent server component keys
527530
implicitSlot: boolean, // true if the root server component of this sequence had a null key
531+
formatContext: FormatContext, // an approximate parent context from host components
528532
thenableState: ThenableState | null,
529533
timed: boolean, // Profiling-only. Whether we need to track the completion time of this task.
530534
time: number, // Profiling-only. The last time stamp emitted for this task.
@@ -758,6 +762,7 @@ function RequestInstance(
758762
model,
759763
null,
760764
false,
765+
createRootFormatContext(),
761766
abortSet,
762767
timeOrigin,
763768
null,
@@ -980,6 +985,7 @@ function serializeThenable(
980985
(thenable: any), // will be replaced by the value before we retry. used for debug info.
981986
task.keyPath, // the server component sequence continues through Promise-as-a-child.
982987
task.implicitSlot,
988+
task.formatContext,
983989
request.abortableTasks,
984990
enableProfilerTimer &&
985991
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -1102,6 +1108,7 @@ function serializeReadableStream(
11021108
task.model,
11031109
task.keyPath,
11041110
task.implicitSlot,
1111+
task.formatContext,
11051112
request.abortableTasks,
11061113
enableProfilerTimer &&
11071114
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -1197,6 +1204,7 @@ function serializeAsyncIterable(
11971204
task.model,
11981205
task.keyPath,
11991206
task.implicitSlot,
1207+
task.formatContext,
12001208
request.abortableTasks,
12011209
enableProfilerTimer &&
12021210
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2028,6 +2036,7 @@ function deferTask(request: Request, task: Task): ReactJSONValue {
20282036
task.model, // the currently rendering element
20292037
task.keyPath, // unlike outlineModel this one carries along context
20302038
task.implicitSlot,
2039+
task.formatContext,
20312040
request.abortableTasks,
20322041
enableProfilerTimer &&
20332042
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2048,6 +2057,7 @@ function outlineTask(request: Request, task: Task): ReactJSONValue {
20482057
task.model, // the currently rendering element
20492058
task.keyPath, // unlike outlineModel this one carries along context
20502059
task.implicitSlot,
2060+
task.formatContext,
20512061
request.abortableTasks,
20522062
enableProfilerTimer &&
20532063
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -2214,6 +2224,22 @@ function renderElement(
22142224
}
22152225
}
22162226
}
2227+
} else if (typeof type === 'string') {
2228+
const parentFormatContext = task.formatContext;
2229+
const newFormatContext = getChildFormatContext(
2230+
parentFormatContext,
2231+
type,
2232+
props,
2233+
);
2234+
if (parentFormatContext !== newFormatContext && props.children != null) {
2235+
// We've entered a new context. We need to create another Task which has
2236+
// the new context set up since it's not safe to push/pop in the middle of
2237+
// a tree. Additionally this means that any deduping within this tree now
2238+
// assumes the new context even if it's reused outside in a different context.
2239+
// We'll rely on this to dedupe the value later as we discover it again
2240+
// inside the returned element's tree.
2241+
outlineModelWithFormatContext(request, props.children, newFormatContext);
2242+
}
22172243
}
22182244
// For anything else, try it on the client instead.
22192245
// We don't know if the client will support it or not. This might error on the
@@ -2530,6 +2556,7 @@ function createTask(
25302556
model: ReactClientValue,
25312557
keyPath: null | string,
25322558
implicitSlot: boolean,
2559+
formatContext: FormatContext,
25332560
abortSet: Set<Task>,
25342561
lastTimestamp: number, // Profiling-only
25352562
debugOwner: null | ReactComponentInfo, // DEV-only
@@ -2554,6 +2581,7 @@ function createTask(
25542581
model,
25552582
keyPath,
25562583
implicitSlot,
2584+
formatContext: formatContext,
25572585
ping: () => pingTask(request, task),
25582586
toJSON: function (
25592587
this:
@@ -2819,11 +2847,26 @@ function serializeDebugClientReference(
28192847
}
28202848

28212849
function outlineModel(request: Request, value: ReactClientValue): number {
2850+
return outlineModelWithFormatContext(
2851+
request,
2852+
value,
2853+
// For deduped values we don't know which context it will be reused in
2854+
// so we have to assume that it's the root context.
2855+
createRootFormatContext(),
2856+
);
2857+
}
2858+
2859+
function outlineModelWithFormatContext(
2860+
request: Request,
2861+
value: ReactClientValue,
2862+
formatContext: FormatContext,
2863+
): number {
28222864
const newTask = createTask(
28232865
request,
28242866
value,
28252867
null, // The way we use outlining is for reusing an object.
28262868
false, // It makes no sense for that use case to be contextual.
2869+
formatContext, // Except for FormatContext we optimistically use it.
28272870
request.abortableTasks,
28282871
enableProfilerTimer &&
28292872
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -3071,6 +3114,7 @@ function serializeBlob(request: Request, blob: Blob): string {
30713114
model,
30723115
null,
30733116
false,
3117+
createRootFormatContext(),
30743118
request.abortableTasks,
30753119
enableProfilerTimer &&
30763120
(enableComponentPerformanceTrack || enableAsyncDebugInfo)
@@ -3208,6 +3252,7 @@ function renderModel(
32083252
task.model,
32093253
task.keyPath,
32103254
task.implicitSlot,
3255+
task.formatContext,
32113256
request.abortableTasks,
32123257
enableProfilerTimer &&
32133258
(enableComponentPerformanceTrack || enableAsyncDebugInfo)

packages/react-server/src/forks/ReactFlightServerConfig.custom.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
3232
export function createHints(): any {
3333
return null;
3434
}
35+
36+
export type FormatContext = null;
37+
38+
export function createRootFormatContext(): FormatContext {
39+
return null;
40+
}
41+
42+
export function getChildFormatContext(
43+
parentContext: FormatContext,
44+
type: string,
45+
props: Object,
46+
): FormatContext {
47+
return parentContext;
48+
}

packages/react-server/src/forks/ReactFlightServerConfig.dom-legacy.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ export const componentStorage: AsyncLocalStorage<ReactComponentInfo | void> =
3232
export function createHints(): any {
3333
return null;
3434
}
35+
36+
export type FormatContext = null;
37+
38+
export function createRootFormatContext(): FormatContext {
39+
return null;
40+
}
41+
42+
export function getChildFormatContext(
43+
parentContext: FormatContext,
44+
type: string,
45+
props: Object,
46+
): FormatContext {
47+
return parentContext;
48+
}

packages/react-server/src/forks/ReactFlightServerConfig.markup.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ export function createHints(): Hints {
1919
return null;
2020
}
2121

22+
export type FormatContext = null;
23+
24+
export function createRootFormatContext(): FormatContext {
25+
return null;
26+
}
27+
28+
export function getChildFormatContext(
29+
parentContext: FormatContext,
30+
type: string,
31+
props: Object,
32+
): FormatContext {
33+
return parentContext;
34+
}
35+
2236
export const supportsRequestStorage = false;
2337
export const requestStorage: AsyncLocalStorage<Request | void> = (null: any);
2438

0 commit comments

Comments
 (0)