Skip to content

Commit a49e59d

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 a49e59d

File tree

55 files changed

+941
-185
lines changed

Some content is hidden

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

55 files changed

+941
-185
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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 {emitHint} from 'react-server/src/ReactFlightServer';
21+
22+
export const ReactDOMFlightServerDispatcher: HostDispatcher = {
23+
prefetchDNS,
24+
preconnect,
25+
preload,
26+
preinit,
27+
};
28+
29+
function prefetchDNS(href: string, options?: ?PrefetchDNSOptions) {
30+
if (enableFloat) {
31+
if (typeof href === 'string') {
32+
// We want to pass through options for validation purposes.
33+
// We could __DEV__ gate this but currently consider keeping the serialization
34+
// identical in prod safer
35+
if (options) {
36+
emitHint('D', [href, options]);
37+
} else {
38+
emitHint('D', href);
39+
}
40+
}
41+
}
42+
}
43+
44+
function preconnect(href: string, options: ?PreconnectOptions) {
45+
if (enableFloat) {
46+
if (typeof href === 'string') {
47+
if (options) {
48+
emitHint('C', [href, options]);
49+
} else {
50+
emitHint('C', href);
51+
}
52+
}
53+
}
54+
}
55+
56+
function preload(href: string, options: PreloadOptions) {
57+
if (enableFloat) {
58+
if (typeof href === 'string') {
59+
emitHint('L', [href, options]);
60+
}
61+
}
62+
}
63+
64+
function preinit(href: string, options: PreinitOptions): void {
65+
if (enableFloat) {
66+
if (typeof href === 'string') {
67+
emitHint('I', [href, options]);
68+
}
69+
}
70+
}

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(

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

Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import {
3838
stringToPrecomputedChunk,
3939
clonePrecomputedChunk,
4040
} from 'react-server/src/ReactServerStreamConfig';
41+
import {resolveResources, pingRequest} from 'react-server/src/ReactFizzServer';
4142

4243
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
4344
import isUnitlessNumber from '../shared/isUnitlessNumber';
@@ -79,30 +80,15 @@ import {
7980
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
8081
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
8182

82-
const ReactDOMServerDispatcher = enableFloat
83-
? {
84-
prefetchDNS,
85-
preconnect,
86-
preload,
87-
preinit,
88-
}
89-
: {};
90-
91-
let currentResources: null | Resources = null;
92-
const currentResourcesStack = [];
93-
94-
export function prepareToRender(resources: Resources): mixed {
95-
currentResourcesStack.push(currentResources);
96-
currentResources = resources;
83+
const ReactDOMServerDispatcher = {
84+
prefetchDNS,
85+
preconnect,
86+
preload,
87+
preinit,
88+
};
9789

98-
const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
90+
export function prepareHostDispatcher() {
9991
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
100-
return previousHostDispatcher;
101-
}
102-
103-
export function cleanupAfterRender(previousDispatcher: mixed) {
104-
currentResources = currentResourcesStack.pop();
105-
ReactDOMCurrentDispatcher.current = previousDispatcher;
10692
}
10793

10894
// Used to distinguish these contexts from ones used in other renderers.
@@ -4804,16 +4790,18 @@ function getResourceKey(as: string, href: string): string {
48044790
}
48054791

48064792
export function prefetchDNS(href: string, options?: mixed) {
4807-
if (!currentResources) {
4808-
// While we expect that preconnect calls are primarily going to be observed
4809-
// during render because effects and events don't run on the server it is
4810-
// still possible that these get called in module scope. This is valid on
4811-
// the client since there is still a document to interact with but on the
4812-
// server we need a request to associate the call to. Because of this we
4813-
// simply return and do not warn.
4793+
if (!enableFloat) {
4794+
return;
4795+
}
4796+
const resources = resolveResources();
4797+
if (!resources) {
4798+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4799+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4800+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4801+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4802+
// fetching) and we don't want to warn in those cases.
48144803
return;
48154804
}
4816-
const resources = currentResources;
48174805
if (__DEV__) {
48184806
if (typeof href !== 'string' || !href) {
48194807
console.error(
@@ -4858,17 +4846,19 @@ export function prefetchDNS(href: string, options?: mixed) {
48584846
}
48594847
}
48604848

4861-
export function preconnect(href: string, options?: {crossOrigin?: string}) {
4862-
if (!currentResources) {
4863-
// While we expect that preconnect calls are primarily going to be observed
4864-
// during render because effects and events don't run on the server it is
4865-
// still possible that these get called in module scope. This is valid on
4866-
// the client since there is still a document to interact with but on the
4867-
// server we need a request to associate the call to. Because of this we
4868-
// simply return and do not warn.
4849+
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
4850+
if (!enableFloat) {
4851+
return;
4852+
}
4853+
const resources = resolveResources();
4854+
if (!resources) {
4855+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4856+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4857+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4858+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4859+
// fetching) and we don't want to warn in those cases.
48694860
return;
48704861
}
4871-
const resources = currentResources;
48724862
if (__DEV__) {
48734863
if (typeof href !== 'string' || !href) {
48744864
console.error(
@@ -4917,24 +4907,25 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
49174907
}
49184908
}
49194909

4920-
type PreloadAs = 'style' | 'font' | 'script';
49214910
type PreloadOptions = {
4922-
as: PreloadAs,
4911+
as: string,
49234912
crossOrigin?: string,
49244913
integrity?: string,
49254914
type?: string,
49264915
};
49274916
export function preload(href: string, options: PreloadOptions) {
4928-
if (!currentResources) {
4929-
// While we expect that preload calls are primarily going to be observed
4930-
// during render because effects and events don't run on the server it is
4931-
// still possible that these get called in module scope. This is valid on
4932-
// the client since there is still a document to interact with but on the
4933-
// server we need a request to associate the call to. Because of this we
4934-
// simply return and do not warn.
4917+
if (!enableFloat) {
4918+
return;
4919+
}
4920+
const resources = resolveResources();
4921+
if (!resources) {
4922+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4923+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4924+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4925+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4926+
// fetching) and we don't want to warn in those cases.
49354927
return;
49364928
}
4937-
const resources = currentResources;
49384929
if (__DEV__) {
49394930
if (typeof href !== 'string' || !href) {
49404931
console.error(
@@ -5055,27 +5046,30 @@ export function preload(href: string, options: PreloadOptions) {
50555046
resources.explicitOtherPreloads.add(resource);
50565047
}
50575048
}
5049+
pingRequest();
50585050
}
50595051
}
50605052

5061-
type PreinitAs = 'style' | 'script';
50625053
type PreinitOptions = {
5063-
as: PreinitAs,
5054+
as: string,
50645055
precedence?: string,
50655056
crossOrigin?: string,
50665057
integrity?: string,
50675058
};
50685059
export function preinit(href: string, options: PreinitOptions): void {
5069-
if (!currentResources) {
5070-
// While we expect that preinit calls are primarily going to be observed
5071-
// during render because effects and events don't run on the server it is
5072-
// still possible that these get called in module scope. This is valid on
5073-
// the client since there is still a document to interact with but on the
5074-
// server we need a request to associate the call to. Because of this we
5075-
// simply return and do not warn.
5060+
if (!enableFloat) {
5061+
return;
5062+
}
5063+
const resources = resolveResources();
5064+
if (!resources) {
5065+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5066+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5067+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
5068+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5069+
// fetching) and we don't want to warn in those cases.
50765070
return;
50775071
}
5078-
preinitImpl(currentResources, href, options);
5072+
preinitImpl(resources, href, options);
50795073
}
50805074

50815075
// On the server, preinit may be called outside of render when sending an
@@ -5297,7 +5291,7 @@ function preinitImpl(
52975291

52985292
function preloadPropsFromPreloadOptions(
52995293
href: string,
5300-
as: PreloadAs,
5294+
as: string,
53015295
options: PreloadOptions,
53025296
): PreloadProps {
53035297
return {

0 commit comments

Comments
 (0)