Skip to content

Commit 2365024

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 b5934f2 commit 2365024

File tree

56 files changed

+927
-105
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

+927
-105
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
preloadModule,
2424
requireModule,
2525
parseModel,
26+
dispatchDirective,
2627
} from './ReactFlightClientHostConfig';
2728

2829
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -758,6 +759,15 @@ export function resolveErrorDev(
758759
}
759760
}
760761

762+
export function resolveDirective(
763+
response: Response,
764+
id: number,
765+
model: string,
766+
): void {
767+
const payload = JSON.parse(model);
768+
dispatchDirective(payload);
769+
}
770+
761771
export function close(response: Response): void {
762772
// In case there are any remaining unresolved chunks, they won't
763773
// be resolved now. So we need to issue an error to those.

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
resolveModel,
1717
resolveErrorProd,
1818
resolveErrorDev,
19+
resolveDirective,
1920
createResponse as createResponseBase,
2021
parseModelString,
2122
parseModelTuple,
@@ -46,6 +47,10 @@ function processFullRow(response: Response, row: string): void {
4647
resolveModule(response, id, row.substring(colon + 2));
4748
return;
4849
}
50+
case 'D': {
51+
resolveDirective(response, id, row.substring(colon + 2));
52+
return;
53+
}
4954
case 'E': {
5055
const errorInfo = JSON.parse(row.substring(colon + 2));
5156
if (__DEV__) {

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

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

3940
export opaque type Source = mixed;
4041

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-browser.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
13+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-bun.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
12+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';
1213

1314
export type Response = any;
1415
export opaque type SSRManifest = mixed;

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-edge-webpack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
13+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-legacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export * from 'react-client/src/ReactFlightClientHostConfigBrowser';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
13+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node-webpack.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export * from 'react-client/src/ReactFlightClientHostConfigNode';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientWebpackBundlerConfig';
13+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-node.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@
1010
export * from 'react-client/src/ReactFlightClientHostConfigNode';
1111
export * from 'react-client/src/ReactFlightClientHostConfigStream';
1212
export * from 'react-server-dom-webpack/src/ReactFlightClientNodeBundlerConfig';
13+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

packages/react-client/src/forks/ReactFlightClientHostConfig.dom-relay.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@
99

1010
export * from 'react-server-dom-relay/src/ReactFlightDOMRelayClientHostConfig';
1111
export * from '../ReactFlightClientHostConfigNoStream';
12+
export * from 'react-dom-bindings/src/shared/ReactDOMFlightClientHostConfig';

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

Lines changed: 6 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';
@@ -1865,10 +1866,6 @@ export function clearSingleton(instance: Instance): void {
18651866

18661867
export const supportsResources = true;
18671868

1868-
// The resource types we support. currently they match the form for the as argument.
1869-
// In the future this may need to change, especially when modules / scripts are supported
1870-
type ResourceType = 'style' | 'font' | 'script';
1871-
18721869
type HoistableTagType = 'link' | 'meta' | 'title';
18731870
type TResource<
18741871
T: 'stylesheet' | 'style' | 'script' | 'void',
@@ -1959,7 +1956,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
19591956
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
19601957
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
19611958
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
1962-
export const ReactDOMClientDispatcher = {
1959+
export const ReactDOMClientDispatcher: HostDispatcher = {
19631960
prefetchDNS,
19641961
preconnect,
19651962
preload,
@@ -2033,7 +2030,7 @@ function prefetchDNS(href: string, options?: mixed) {
20332030
preconnectAs('dns-prefetch', null, href);
20342031
}
20352032
2036-
function preconnect(href: string, options?: {crossOrigin?: string}) {
2033+
function preconnect(href: string, options?: ?{crossOrigin?: string}) {
20372034
if (!enableFloat) {
20382035
return;
20392036
}
@@ -2064,9 +2061,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
20642061
preconnectAs('preconnect', crossOrigin, href);
20652062
}
20662063
2067-
type PreloadAs = ResourceType;
20682064
type PreloadOptions = {
2069-
as: PreloadAs,
2065+
as: string,
20702066
crossOrigin?: string,
20712067
integrity?: string,
20722068
type?: string,
@@ -2115,7 +2111,7 @@ function preload(href: string, options: PreloadOptions) {
21152111
21162112
function preloadPropsFromPreloadOptions(
21172113
href: string,
2118-
as: ResourceType,
2114+
as: string,
21192115
options: PreloadOptions,
21202116
): PreloadProps {
21212117
return {
@@ -2128,9 +2124,8 @@ function preloadPropsFromPreloadOptions(
21282124
};
21292125
}
21302126
2131-
type PreinitAs = 'style' | 'script';
21322127
type PreinitOptions = {
2133-
as: PreinitAs,
2128+
as: string,
21342129
precedence?: string,
21352130
crossOrigin?: string,
21362131
integrity?: string,
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 {enableFloat} from 'shared/ReactFeatureFlags';
11+
12+
import {resolveDirectives} from 'react-server/src/ReactFlightDirectives';
13+
14+
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
15+
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
16+
17+
const ReactDOMFlightServerDispatcher = {
18+
prefetchDNS,
19+
preconnect,
20+
preload,
21+
preinit,
22+
};
23+
24+
export function prepareHostDispatcher(): void {
25+
ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher;
26+
}
27+
28+
// Used to distinguish these contexts from ones used in other renderers.
29+
// E.g. this can be used to distinguish legacy renderers from this modern one.
30+
export const isPrimaryRenderer = true;
31+
32+
let didWarnAsyncEnvironmentDev = false;
33+
34+
export function prefetchDNS(href: string, options?: mixed) {
35+
if (enableFloat) {
36+
pushDirective('prefetchDNS', href, options);
37+
}
38+
}
39+
40+
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
41+
if (enableFloat) {
42+
pushDirective('preconnect', href, options);
43+
}
44+
}
45+
46+
type PreloadOptions = {
47+
as: string,
48+
crossOrigin?: string,
49+
integrity?: string,
50+
type?: string,
51+
};
52+
53+
export function preload(href: string, options: PreloadOptions) {
54+
if (enableFloat) {
55+
pushDirective('preload', href, options);
56+
}
57+
}
58+
59+
type PreinitOptions = {
60+
as: string,
61+
precedence?: string,
62+
crossOrigin?: string,
63+
integrity?: string,
64+
};
65+
export function preinit(href: string, options: PreinitOptions): void {
66+
if (enableFloat) {
67+
pushDirective('preinit', href, options);
68+
}
69+
}
70+
71+
function pushDirective(
72+
method: 'prefetchDNS' | 'preconnect' | 'preload' | 'preinit',
73+
href: string,
74+
options: mixed,
75+
): void {
76+
const directives = resolveDirectives();
77+
if (directives === null) {
78+
if (__DEV__) {
79+
if (!didWarnAsyncEnvironmentDev) {
80+
didWarnAsyncEnvironmentDev = true;
81+
console.error(
82+
'ReactDOM.%s(): React expected to be able to associate this call to a specific Request but cannot. It is possible that this call was invoked outside of a React component. If you are calling it from within a React component that is an async function after the first `await` then you are in an environment which does not support AsyncLocalStorage. In this kind of environment ReactDOM.%s() does not do anything when called in an async manner. Try moving this function call above the first `await` within the component or remove this call. In environments that support AsyncLocalStorage such as Node.js you can call this method anywhere in a React component even after `await` operator.',
83+
method,
84+
method,
85+
);
86+
}
87+
}
88+
return;
89+
}
90+
// @TODO need to escape
91+
directives.push(JSON.stringify({method, args: [href, options]}));
92+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export function flushBuffered(destination: Destination) {}
2323

2424
export const supportsRequestStorage = false;
2525
export const requestStorage: AsyncLocalStorage<any> = (null: any);
26+
export const requestStorage2: AsyncLocalStorage<any> = (null: any);
2627

2728
export function beginWriting(destination: Destination) {}
2829

0 commit comments

Comments
 (0)