Skip to content

Commit 54f91f1

Browse files
committed
Flight support for Float
Supporting Float methods such as ReactDOM.preload() are challenging for flight because it does not have an easy means to convey direct executions in other environments. Because the flight wire format is a JSON-like serialization that is expected to be rendered it currently only describes renderable elements. We need a way to convey a function invocation that gets run in the context of the client environment whether that is Fizz or Fiber. Fiber is somewhat straightforward because the HostDispatcher is always active and we can just have the FlightClient dispatch the serialized directive. Fizz is much more challenging becaue the dispatcher is always scoped but the specific request the dispatch belongs to is not readily available. Environments that support AsyncLocalStorage (or in the future AsyncContext) we will use this to be able to resolve directives in Fizz to the appropriate Request. For other environments directives will be elided. Right now this is pragmatic and non-breaking because all directives are opportunistic and non-critical. If this changes in the future we will need to reconsider how widespread support for async context tracking is. For Flight, if AsyncLocalStorage is available Float methods can be called before and after await points and be expected to work. If AsyncLocalStorage is not available float methods called in the sync phase of a component render will be captured but anything after an await point will be a noop. If a float call is dropped in this manner a DEV warning should help you realize your code may need to be modified.
1 parent fd3fb8e commit 54f91f1

File tree

56 files changed

+1119
-188
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1119
-188
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@ import type {
1818
SSRManifest,
1919
} from './ReactFlightClientConfig';
2020

21+
import type {HintModel} from 'react-server/src/ReactFlightServerConfig';
22+
2123
import {
2224
resolveClientReference,
2325
preloadModule,
2426
requireModule,
2527
parseModel,
28+
dispatchHint,
2629
} from './ReactFlightClientConfig';
2730

2831
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -778,6 +781,15 @@ export function resolveErrorDev(
778781
}
779782
}
780783

784+
export function resolveHint(
785+
response: Response,
786+
code: string,
787+
model: UninitializedModel,
788+
): void {
789+
const hintModel = parseModel<HintModel>(response, model);
790+
dispatchHint(code, hintModel);
791+
}
792+
781793
export function close(response: Response): void {
782794
// In case there are any remaining unresolved chunks, they won't
783795
// be resolved now. So we need to issue an error to those.

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveModel,
1717
resolveErrorProd,
1818
resolveErrorDev,
19+
resolveHint,
1920
createResponse as createResponseBase,
2021
parseModelString,
2122
parseModelTuple,
@@ -46,6 +47,11 @@ function processFullRow(response: Response, row: string): void {
4647
resolveModule(response, id, row.slice(colon + 2));
4748
return;
4849
}
50+
case 'H': {
51+
const code = row[colon + 2];
52+
resolveHint(response, code, row.slice(colon + 3));
53+
return;
54+
}
4955
case 'E': {
5056
const errorInfo = JSON.parse(row.slice(colon + 2));
5157
if (__DEV__) {

packages/react-client/src/forks/ReactFlightClientConfig.custom.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
3535
export const resolveServerReference = $$$config.resolveServerReference;
3636
export const preloadModule = $$$config.preloadModule;
3737
export const requireModule = $$$config.requireModule;
38+
export const dispatchHint = $$$config.dispatchHint;
3839

3940
export opaque type Source = mixed;
4041

packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher';
1011
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
1112
import type {DOMEventName} from '../events/DOMEventNames';
1213
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
@@ -1868,10 +1869,6 @@ export function clearSingleton(instance: Instance): void {
18681869

18691870
export const supportsResources = true;
18701871

1871-
// The resource types we support. currently they match the form for the as argument.
1872-
// In the future this may need to change, especially when modules / scripts are supported
1873-
type ResourceType = 'style' | 'font' | 'script';
1874-
18751872
type HoistableTagType = 'link' | 'meta' | 'title';
18761873
type TResource<
18771874
T: 'stylesheet' | 'style' | 'script' | 'void',
@@ -1962,7 +1959,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
19621959
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
19631960
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
19641961
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
1965-
export const ReactDOMClientDispatcher = {
1962+
export const ReactDOMClientDispatcher: HostDispatcher = {
19661963
prefetchDNS,
19671964
preconnect,
19681965
preload,
@@ -2036,7 +2033,10 @@ function prefetchDNS(href: string, options?: mixed) {
20362033
preconnectAs('dns-prefetch', null, href);
20372034
}
20382035
2039-
function preconnect(href: string, options?: {crossOrigin?: string}) {
2036+
function preconnect(href: string, options: ?{crossOrigin?: string}) {
2037+
if (!enableFloat) {
2038+
return;
2039+
}
20402040
if (__DEV__) {
20412041
if (typeof href !== 'string' || !href) {
20422042
console.error(
@@ -2064,9 +2064,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
20642064
preconnectAs('preconnect', crossOrigin, href);
20652065
}
20662066
2067-
type PreloadAs = ResourceType;
20682067
type PreloadOptions = {
2069-
as: PreloadAs,
2068+
as: string,
20702069
crossOrigin?: string,
20712070
integrity?: string,
20722071
type?: string,
@@ -2115,7 +2114,7 @@ function preload(href: string, options: PreloadOptions) {
21152114
21162115
function preloadPropsFromPreloadOptions(
21172116
href: string,
2118-
as: ResourceType,
2117+
as: string,
21192118
options: PreloadOptions,
21202119
): PreloadProps {
21212120
return {
@@ -2128,9 +2127,8 @@ function preloadPropsFromPreloadOptions(
21282127
};
21292128
}
21302129
2131-
type PreinitAs = 'style' | 'script';
21322130
type PreinitOptions = {
2133-
as: PreinitAs,
2131+
as: string,
21342132
precedence?: string,
21352133
crossOrigin?: string,
21362134
integrity?: string,
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {
11+
HostDispatcher,
12+
PrefetchDNSOptions,
13+
PreconnectOptions,
14+
PreloadOptions,
15+
PreinitOptions,
16+
} from 'react-dom/src/ReactDOMDispatcher';
17+
18+
import {enableFloat} from 'shared/ReactFeatureFlags';
19+
20+
import {
21+
emitHint,
22+
getHints,
23+
resolveRequest,
24+
} from 'react-server/src/ReactFlightServer';
25+
26+
export const ReactDOMFlightServerDispatcher: HostDispatcher = {
27+
prefetchDNS,
28+
preconnect,
29+
preload,
30+
preinit,
31+
};
32+
33+
function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
34+
if (enableFloat) {
35+
if (typeof href === 'string') {
36+
const request = resolveRequest();
37+
if (request) {
38+
const hints = getHints(request);
39+
const key = 'D' + href;
40+
if (hints.has(key)) {
41+
// duplicate hint
42+
return;
43+
}
44+
hints.add(key);
45+
if (options) {
46+
emitHint(request, 'D', [href, options]);
47+
} else {
48+
emitHint(request, 'D', href);
49+
}
50+
}
51+
}
52+
}
53+
}
54+
55+
function preconnect(href: string, options: ?PreconnectOptions) {
56+
if (enableFloat) {
57+
if (typeof href === 'string') {
58+
const request = resolveRequest();
59+
if (request) {
60+
const hints = getHints(request);
61+
const crossOrigin =
62+
options == null || typeof options.crossOrigin !== 'string'
63+
? null
64+
: options.crossOrigin === 'use-credentials'
65+
? 'use-credentials'
66+
: '';
67+
68+
const key = `C${crossOrigin === null ? 'null' : crossOrigin}|${href}`;
69+
if (hints.has(key)) {
70+
// duplicate hint
71+
return;
72+
}
73+
hints.add(key);
74+
if (options) {
75+
emitHint(request, 'C', [href, options]);
76+
} else {
77+
emitHint(request, 'C', href);
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
function preload(href: string, options: PreloadOptions) {
85+
if (enableFloat) {
86+
if (typeof href === 'string') {
87+
const request = resolveRequest();
88+
if (request) {
89+
const hints = getHints(request);
90+
const key = 'L' + href;
91+
if (hints.has(key)) {
92+
// duplicate hint
93+
return;
94+
}
95+
hints.add(key);
96+
emitHint(request, 'L', [href, options]);
97+
}
98+
}
99+
}
100+
}
101+
102+
function preinit(href: string, options: PreinitOptions) {
103+
if (enableFloat) {
104+
if (typeof href === 'string') {
105+
const request = resolveRequest();
106+
if (request) {
107+
const hints = getHints(request);
108+
const key = 'I' + href;
109+
if (hints.has(key)) {
110+
// duplicate hint
111+
return;
112+
}
113+
hints.add(key);
114+
emitHint(request, 'I', [href, options]);
115+
}
116+
}
117+
}
118+
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {
2121

2222
export function flushBuffered(destination: Destination) {}
2323

24-
export const supportsRequestStorage = false;
25-
export const requestStorage: AsyncLocalStorage<any> = (null: any);
26-
2724
export function beginWriting(destination: Destination) {}
2825

2926
export function writeChunk(

0 commit comments

Comments
 (0)