Skip to content

Commit 8622483

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. For environments that support AsyncLocalStorage we could use that to provide the right Request context when dispatching the directives as they stream in however it would but ineffective in the environments without this feature. Instead we stash a "Store" on the Flight client request object and then set it just before dispatching and unset it when the dispatch is complete. This commit also adds support for AsyncLocalStorage in Fizz however this is more about supporting Float functions after an await point given we expect to offer support for async function components in this runtime soon. For Flight, if AsyncLocalStorage is available Float methods can be called after await points. If AsyncLocalStorage is not available float methods after await points are noops and you get a warning (in Dev).
1 parent 4a1cc2d commit 8622483

File tree

56 files changed

+1129
-153
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

+1129
-153
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 11 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,16 @@ export function resolveErrorDev(
758759
}
759760
}
760761

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

packages/react-client/src/ReactFlightClientHostConfigStream.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type Response = ResponseBase & {
1414
_partialRow: string,
1515
_fromJSON: (key: string, value: JSONValue) => any,
1616
_stringDecoder: StringDecoder,
17+
_store: mixed,
1718
};
1819

1920
export type UninitializedModel = string;

packages/react-client/src/ReactFlightClientStream.js

Lines changed: 9 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,
@@ -26,6 +27,7 @@ import {
2627
readFinalStringChunk,
2728
supportsBinaryStreams,
2829
createStringDecoder,
30+
resolveStore,
2931
} from './ReactFlightClientHostConfig';
3032

3133
export type {Response};
@@ -46,6 +48,10 @@ function processFullRow(response: Response, row: string): void {
4648
resolveModule(response, id, row.substring(colon + 2));
4749
return;
4850
}
51+
case 'D': {
52+
resolveDirective(response, id, row.substring(colon + 2));
53+
return;
54+
}
4955
case 'E': {
5056
const errorInfo = JSON.parse(row.substring(colon + 2));
5157
if (__DEV__) {
@@ -134,6 +140,9 @@ export function createResponse(
134140
}
135141
// Don't inline this call because it causes closure to outline the call above.
136142
response._fromJSON = createFromJSONCallback(response);
143+
// If the Host provides a store stash it on the response so we can provide it back
144+
// to the dispatcher when resolving directives
145+
response._store = resolveStore();
137146
return response;
138147
}
139148

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ 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 resolveStore = $$$hostConfig.resolveStore;
39+
export const dispatchDirective = $$$hostConfig.dispatchDirective;
3840

3941
export opaque type Source = mixed;
4042

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-client/src/forks/ReactFlightClientHostConfig.native-relay.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@
77
* @flow
88
*/
99

10+
export * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
1011
export * from 'react-server-native-relay/src/ReactFlightNativeRelayClientHostConfig';
1112
export * from '../ReactFlightClientHostConfigNoStream';
13+
export * from 'react-native-renderer/src/shared/ReactNativeFlightClientHostConfig';

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

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

10+
import type {HostDispatcher} from 'react-dom-bindings/src/shared/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';
@@ -1940,23 +1941,14 @@ export function prepareToCommitHoistables() {
19401941
// appropriate for resources that don't really have a strict tie to the document itself for example
19411942
// preloads
19421943
let lastCurrentDocument: ?Document = null;
1943-
let previousDispatcher = null;
19441944
export function prepareRendererToRender(rootContainer: Container) {
19451945
if (enableFloat) {
19461946
const rootNode = getHoistableRoot(rootContainer);
19471947
lastCurrentDocument = getDocumentFromRoot(rootNode);
1948-
1949-
previousDispatcher = Dispatcher.current;
1950-
Dispatcher.current = ReactDOMClientDispatcher;
19511948
}
19521949
}
19531950

1954-
export function resetRendererAfterRender() {
1955-
if (enableFloat) {
1956-
Dispatcher.current = previousDispatcher;
1957-
previousDispatcher = null;
1958-
}
1959-
}
1951+
export function resetRendererAfterRender() {}
19601952

19611953
// global collections of Resources
19621954
const preloadPropsMap: Map<string, PreloadProps> = new Map();
@@ -1979,13 +1971,7 @@ function getCurrentResourceRoot(): null | HoistableRoot {
19791971
return currentContainer ? getHoistableRoot(currentContainer) : null;
19801972
}
19811973

1982-
// Preloads are somewhat special. Even if we don't have the Document
1983-
// used by the root that is rendering a component trying to insert a preload
1984-
// we can still seed the file cache by doing the preload on any document we have
1985-
// access to. We prefer the currentDocument if it exists, we also prefer the
1986-
// lastCurrentDocument if that exists. As a fallback we will use the window.document
1987-
// if available.
1988-
function getDocumentForPreloads(): ?Document {
1974+
function getBestEffortDocument(): ?Document {
19891975
const root = getCurrentResourceRoot();
19901976
if (root) {
19911977
return root.ownerDocument || root;
@@ -1998,14 +1984,27 @@ function getDocumentForPreloads(): ?Document {
19981984
}
19991985
}
20001986

1987+
function getBestEffortRoot(): ?HoistableRoot {
1988+
const root = getCurrentResourceRoot();
1989+
if (root) {
1990+
return root;
1991+
} else {
1992+
try {
1993+
return lastCurrentDocument || window.document;
1994+
} catch (error) {
1995+
return null;
1996+
}
1997+
}
1998+
}
1999+
20012000
function getDocumentFromRoot(root: HoistableRoot): Document {
20022001
return root.ownerDocument || root;
20032002
}
20042003

20052004
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
20062005
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
20072006
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
2008-
export const ReactDOMClientDispatcher = {
2007+
export const ReactDOMClientDispatcher: HostDispatcher = {
20092008
prefetchDNS,
20102009
preconnect,
20112010
preload,
@@ -2017,7 +2016,7 @@ function preconnectAs(
20172016
crossOrigin: null | '' | 'use-credentials',
20182017
href: string,
20192018
) {
2020-
const ownerDocument = getDocumentForPreloads();
2019+
const ownerDocument = getBestEffortDocument();
20212020
if (typeof href === 'string' && href && ownerDocument) {
20222021
const limitedEscapedHref =
20232022
escapeSelectorAttributeValueInsideDoubleQuotes(href);
@@ -2039,7 +2038,7 @@ function preconnectAs(
20392038
}
20402039
}
20412040
2042-
function prefetchDNS(href: string, options?: mixed) {
2041+
function prefetchDNS(href: string, options?: ?mixed) {
20432042
if (__DEV__) {
20442043
if (typeof href !== 'string' || !href) {
20452044
console.error(
@@ -2066,7 +2065,7 @@ function prefetchDNS(href: string, options?: mixed) {
20662065
preconnectAs('dns-prefetch', null, href);
20672066
}
20682067
2069-
function preconnect(href: string, options?: {crossOrigin?: string}) {
2068+
function preconnect(href: string, options?: ?{crossOrigin?: string}) {
20702069
if (__DEV__) {
20712070
if (typeof href !== 'string' || !href) {
20722071
console.error(
@@ -2096,7 +2095,7 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
20962095
20972096
type PreloadAs = ResourceType;
20982097
type PreloadOptions = {
2099-
as: PreloadAs,
2098+
as: string,
21002099
crossOrigin?: string,
21012100
integrity?: string,
21022101
type?: string,
@@ -2105,7 +2104,7 @@ function preload(href: string, options: PreloadOptions) {
21052104
if (__DEV__) {
21062105
validatePreloadArguments(href, options);
21072106
}
2108-
const ownerDocument = getDocumentForPreloads();
2107+
const ownerDocument = getBestEffortDocument();
21092108
if (
21102109
typeof href === 'string' &&
21112110
href &&
@@ -2142,7 +2141,7 @@ function preload(href: string, options: PreloadOptions) {
21422141
21432142
function preloadPropsFromPreloadOptions(
21442143
href: string,
2145-
as: ResourceType,
2144+
as: string,
21462145
options: PreloadOptions,
21472146
): PreloadProps {
21482147
return {
@@ -2157,7 +2156,7 @@ function preloadPropsFromPreloadOptions(
21572156
21582157
type PreinitAs = 'style' | 'script';
21592158
type PreinitOptions = {
2160-
as: PreinitAs,
2159+
as: string,
21612160
precedence?: string,
21622161
crossOrigin?: string,
21632162
integrity?: string,
@@ -2173,45 +2172,10 @@ function preinit(href: string, options: PreinitOptions) {
21732172
typeof options === 'object' &&
21742173
options !== null
21752174
) {
2176-
const resourceRoot = getCurrentResourceRoot();
2175+
const resourceRoot = getBestEffortRoot();
21772176
const as = options.as;
21782177
if (!resourceRoot) {
2179-
if (as === 'style' || as === 'script') {
2180-
// We are going to emit a preload as a best effort fallback since this preinit
2181-
// was called outside of a render. Given the passive nature of this fallback
2182-
// we do not warn in dev when props disagree if there happens to already be a
2183-
// matching preload with this href
2184-
const preloadDocument = getDocumentForPreloads();
2185-
if (preloadDocument) {
2186-
const limitedEscapedHref =
2187-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
2188-
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2189-
let key = preloadKey;
2190-
switch (as) {
2191-
case 'style':
2192-
key = getStyleKey(href);
2193-
break;
2194-
case 'script':
2195-
key = getScriptKey(href);
2196-
break;
2197-
}
2198-
if (!preloadPropsMap.has(key)) {
2199-
const preloadProps = preloadPropsFromPreinitOptions(
2200-
href,
2201-
as,
2202-
options,
2203-
);
2204-
preloadPropsMap.set(key, preloadProps);
2205-
2206-
if (null === preloadDocument.querySelector(preloadKey)) {
2207-
const instance = preloadDocument.createElement('link');
2208-
setInitialProperties(instance, 'link', preloadProps);
2209-
markNodeAsHoistable(instance);
2210-
(preloadDocument.head: any).appendChild(instance);
2211-
}
2212-
}
2213-
}
2214-
}
2178+
// If we don't have a root to preinit into we just do nothing.
22152179
return;
22162180
}
22172181

0 commit comments

Comments
 (0)