Skip to content

Commit 6917df1

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 0bdcfff commit 6917df1

Some content is hidden

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

41 files changed

+914
-109
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 './ReactFlightClientConfig';
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/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 dispatchDirective = $$$config.dispatchDirective;
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';
@@ -1864,10 +1865,6 @@ export function clearSingleton(instance: Instance): void {
18641865

18651866
export const supportsResources = true;
18661867

1867-
// The resource types we support. currently they match the form for the as argument.
1868-
// In the future this may need to change, especially when modules / scripts are supported
1869-
type ResourceType = 'style' | 'font' | 'script';
1870-
18711868
type HoistableTagType = 'link' | 'meta' | 'title';
18721869
type TResource<
18731870
T: 'stylesheet' | 'style' | 'script' | 'void',
@@ -1958,7 +1955,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
19581955
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
19591956
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
19601957
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
1961-
export const ReactDOMClientDispatcher = {
1958+
export const ReactDOMClientDispatcher: HostDispatcher = {
19621959
prefetchDNS,
19631960
preconnect,
19641961
preload,
@@ -2032,7 +2029,10 @@ function prefetchDNS(href: string, options?: mixed) {
20322029
preconnectAs('dns-prefetch', null, href);
20332030
}
20342031
2035-
function preconnect(href: string, options?: {crossOrigin?: string}) {
2032+
function preconnect(href: string, options: ?{crossOrigin?: string}) {
2033+
if (!enableFloat) {
2034+
return;
2035+
}
20362036
if (__DEV__) {
20372037
if (typeof href !== 'string' || !href) {
20382038
console.error(
@@ -2060,9 +2060,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
20602060
preconnectAs('preconnect', crossOrigin, href);
20612061
}
20622062
2063-
type PreloadAs = ResourceType;
20642063
type PreloadOptions = {
2065-
as: PreloadAs,
2064+
as: string,
20662065
crossOrigin?: string,
20672066
integrity?: string,
20682067
type?: string,
@@ -2111,7 +2110,7 @@ function preload(href: string, options: PreloadOptions) {
21112110
21122111
function preloadPropsFromPreloadOptions(
21132112
href: string,
2114-
as: ResourceType,
2113+
as: string,
21152114
options: PreloadOptions,
21162115
): PreloadProps {
21172116
return {
@@ -2124,9 +2123,8 @@ function preloadPropsFromPreloadOptions(
21242123
};
21252124
}
21262125
2127-
type PreinitAs = 'style' | 'script';
21282126
type PreinitOptions = {
2129-
as: PreinitAs,
2127+
as: string,
21302128
precedence?: string,
21312129
crossOrigin?: string,
21322130
integrity?: string,

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

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

Lines changed: 71 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ import {
3737
stringToPrecomputedChunk,
3838
clonePrecomputedChunk,
3939
} from 'react-server/src/ReactServerStreamConfig';
40+
import {
41+
resolveResources,
42+
setCurrentResources,
43+
getCurrentResources,
44+
} from 'react-server/src/ReactFizzResources';
4045

4146
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
4247
import isUnitlessNumber from '../shared/isUnitlessNumber';
@@ -78,30 +83,34 @@ import {
7883
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
7984
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
8085

81-
const ReactDOMServerDispatcher = enableFloat
82-
? {
83-
prefetchDNS,
84-
preconnect,
85-
preload,
86-
preinit,
87-
}
88-
: {};
86+
const ReactDOMServerDispatcher = {
87+
prefetchDNS,
88+
preconnect,
89+
preload,
90+
preinit,
91+
};
8992

90-
let currentResources: null | Resources = null;
9193
const currentResourcesStack = [];
9294

93-
export function prepareToRender(resources: Resources): mixed {
94-
currentResourcesStack.push(currentResources);
95-
currentResources = resources;
95+
function pushResources(resources: null | Resources) {
96+
currentResourcesStack.push(getCurrentResources());
97+
setCurrentResources(resources);
98+
}
9699

97-
const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
100+
function popResources() {
101+
setCurrentResources(currentResourcesStack.pop());
102+
}
103+
104+
export function prepareHostDispatcher() {
98105
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
99-
return previousHostDispatcher;
100106
}
101107

102-
export function cleanupAfterRender(previousDispatcher: mixed) {
103-
currentResources = currentResourcesStack.pop();
104-
ReactDOMCurrentDispatcher.current = previousDispatcher;
108+
export function prepareToRender(resources: Resources): mixed {
109+
pushResources(resources);
110+
}
111+
112+
export function cleanupAfterRender() {
113+
popResources();
105114
}
106115

107116
// Used to distinguish these contexts from ones used in other renderers.
@@ -4490,16 +4499,18 @@ function getResourceKey(as: string, href: string): string {
44904499
}
44914500

44924501
export function prefetchDNS(href: string, options?: mixed) {
4493-
if (!currentResources) {
4494-
// While we expect that preconnect calls are primarily going to be observed
4495-
// during render because effects and events don't run on the server it is
4496-
// still possible that these get called in module scope. This is valid on
4497-
// the client since there is still a document to interact with but on the
4498-
// server we need a request to associate the call to. Because of this we
4499-
// simply return and do not warn.
4502+
if (!enableFloat) {
4503+
return;
4504+
}
4505+
const resources = resolveResources();
4506+
if (!resources) {
4507+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4508+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4509+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4510+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4511+
// fetching) and we don't want to warn in those cases.
45004512
return;
45014513
}
4502-
const resources = currentResources;
45034514
if (__DEV__) {
45044515
if (typeof href !== 'string' || !href) {
45054516
console.error(
@@ -4544,17 +4555,19 @@ export function prefetchDNS(href: string, options?: mixed) {
45444555
}
45454556
}
45464557

4547-
export function preconnect(href: string, options?: {crossOrigin?: string}) {
4548-
if (!currentResources) {
4549-
// While we expect that preconnect calls are primarily going to be observed
4550-
// during render because effects and events don't run on the server it is
4551-
// still possible that these get called in module scope. This is valid on
4552-
// the client since there is still a document to interact with but on the
4553-
// server we need a request to associate the call to. Because of this we
4554-
// simply return and do not warn.
4558+
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
4559+
if (!enableFloat) {
4560+
return;
4561+
}
4562+
const resources = resolveResources();
4563+
if (!resources) {
4564+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4565+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4566+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4567+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4568+
// fetching) and we don't want to warn in those cases.
45554569
return;
45564570
}
4557-
const resources = currentResources;
45584571
if (__DEV__) {
45594572
if (typeof href !== 'string' || !href) {
45604573
console.error(
@@ -4603,24 +4616,25 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
46034616
}
46044617
}
46054618

4606-
type PreloadAs = 'style' | 'font' | 'script';
46074619
type PreloadOptions = {
4608-
as: PreloadAs,
4620+
as: string,
46094621
crossOrigin?: string,
46104622
integrity?: string,
46114623
type?: string,
46124624
};
46134625
export function preload(href: string, options: PreloadOptions) {
4614-
if (!currentResources) {
4615-
// While we expect that preload calls are primarily going to be observed
4616-
// during render because effects and events don't run on the server it is
4617-
// still possible that these get called in module scope. This is valid on
4618-
// the client since there is still a document to interact with but on the
4619-
// server we need a request to associate the call to. Because of this we
4620-
// simply return and do not warn.
4626+
if (!enableFloat) {
4627+
return;
4628+
}
4629+
const resources = resolveResources();
4630+
if (!resources) {
4631+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4632+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4633+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4634+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4635+
// fetching) and we don't want to warn in those cases.
46214636
return;
46224637
}
4623-
const resources = currentResources;
46244638
if (__DEV__) {
46254639
if (typeof href !== 'string' || !href) {
46264640
console.error(
@@ -4744,24 +4758,26 @@ export function preload(href: string, options: PreloadOptions) {
47444758
}
47454759
}
47464760

4747-
type PreinitAs = 'style' | 'script';
47484761
type PreinitOptions = {
4749-
as: PreinitAs,
4762+
as: string,
47504763
precedence?: string,
47514764
crossOrigin?: string,
47524765
integrity?: string,
47534766
};
47544767
export function preinit(href: string, options: PreinitOptions): void {
4755-
if (!currentResources) {
4756-
// While we expect that preinit calls are primarily going to be observed
4757-
// during render because effects and events don't run on the server it is
4758-
// still possible that these get called in module scope. This is valid on
4759-
// the client since there is still a document to interact with but on the
4760-
// server we need a request to associate the call to. Because of this we
4761-
// simply return and do not warn.
4768+
if (!enableFloat) {
47624769
return;
47634770
}
4764-
preinitImpl(currentResources, href, options);
4771+
const resources = resolveResources();
4772+
if (!resources) {
4773+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4774+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4775+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4776+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4777+
// fetching) and we don't want to warn in those cases.
4778+
return;
4779+
}
4780+
preinitImpl(resources, href, options);
47654781
}
47664782

47674783
// On the server, preinit may be called outside of render when sending an
@@ -4983,7 +4999,7 @@ function preinitImpl(
49834999

49845000
function preloadPropsFromPreloadOptions(
49855001
href: string,
4986-
as: PreloadAs,
5002+
as: string,
49875003
options: PreloadOptions,
49885004
): PreloadProps {
49895005
return {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ export {
137137
writePostamble,
138138
hoistResources,
139139
setCurrentlyRenderingBoundaryResourcesTarget,
140+
prepareHostDispatcher,
140141
prepareToRender,
141142
cleanupAfterRender,
142143
} from './ReactFizzConfigDOM';

0 commit comments

Comments
 (0)