Skip to content

Commit 0a01185

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 fdad813 commit 0a01185

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';
@@ -778,6 +779,15 @@ export function resolveErrorDev(
778779
}
779780
}
780781

782+
export function resolveDirective(
783+
response: Response,
784+
id: number,
785+
model: string,
786+
): void {
787+
const payload = JSON.parse(model);
788+
dispatchDirective(payload);
789+
}
790+
781791
export function close(response: Response): void {
782792
// In case there are any remaining unresolved chunks, they won't
783793
// 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.slice(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.slice(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';
@@ -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,

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
@@ -38,6 +38,11 @@ import {
3838
stringToPrecomputedChunk,
3939
clonePrecomputedChunk,
4040
} from 'react-server/src/ReactServerStreamConfig';
41+
import {
42+
resolveResources,
43+
setCurrentResources,
44+
getCurrentResources,
45+
} from 'react-server/src/ReactFizzResources';
4146

4247
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
4348
import isUnitlessNumber from '../shared/isUnitlessNumber';
@@ -79,30 +84,34 @@ import {
7984
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
8085
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
8186

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

91-
let currentResources: null | Resources = null;
9294
const currentResourcesStack = [];
9395

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

98-
const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
101+
function popResources() {
102+
setCurrentResources(currentResourcesStack.pop());
103+
}
104+
105+
export function prepareHostDispatcher() {
99106
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
100-
return previousHostDispatcher;
101107
}
102108

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

108117
// Used to distinguish these contexts from ones used in other renderers.
@@ -4804,16 +4813,18 @@ function getResourceKey(as: string, href: string): string {
48044813
}
48054814

48064815
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.
4816+
if (!enableFloat) {
4817+
return;
4818+
}
4819+
const resources = resolveResources();
4820+
if (!resources) {
4821+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4822+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4823+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4824+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4825+
// fetching) and we don't want to warn in those cases.
48144826
return;
48154827
}
4816-
const resources = currentResources;
48174828
if (__DEV__) {
48184829
if (typeof href !== 'string' || !href) {
48194830
console.error(
@@ -4858,17 +4869,19 @@ export function prefetchDNS(href: string, options?: mixed) {
48584869
}
48594870
}
48604871

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.
4872+
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
4873+
if (!enableFloat) {
4874+
return;
4875+
}
4876+
const resources = resolveResources();
4877+
if (!resources) {
4878+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4879+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4880+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4881+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4882+
// fetching) and we don't want to warn in those cases.
48694883
return;
48704884
}
4871-
const resources = currentResources;
48724885
if (__DEV__) {
48734886
if (typeof href !== 'string' || !href) {
48744887
console.error(
@@ -4917,24 +4930,25 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
49174930
}
49184931
}
49194932

4920-
type PreloadAs = 'style' | 'font' | 'script';
49214933
type PreloadOptions = {
4922-
as: PreloadAs,
4934+
as: string,
49234935
crossOrigin?: string,
49244936
integrity?: string,
49254937
type?: string,
49264938
};
49274939
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.
4940+
if (!enableFloat) {
4941+
return;
4942+
}
4943+
const resources = resolveResources();
4944+
if (!resources) {
4945+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4946+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4947+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4948+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4949+
// fetching) and we don't want to warn in those cases.
49354950
return;
49364951
}
4937-
const resources = currentResources;
49384952
if (__DEV__) {
49394953
if (typeof href !== 'string' || !href) {
49404954
console.error(
@@ -5058,24 +5072,26 @@ export function preload(href: string, options: PreloadOptions) {
50585072
}
50595073
}
50605074

5061-
type PreinitAs = 'style' | 'script';
50625075
type PreinitOptions = {
5063-
as: PreinitAs,
5076+
as: string,
50645077
precedence?: string,
50655078
crossOrigin?: string,
50665079
integrity?: string,
50675080
};
50685081
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.
5082+
if (!enableFloat) {
50765083
return;
50775084
}
5078-
preinitImpl(currentResources, href, options);
5085+
const resources = resolveResources();
5086+
if (!resources) {
5087+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5088+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5089+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
5090+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5091+
// fetching) and we don't want to warn in those cases.
5092+
return;
5093+
}
5094+
preinitImpl(resources, href, options);
50795095
}
50805096

50815097
// On the server, preinit may be called outside of render when sending an
@@ -5297,7 +5313,7 @@ function preinitImpl(
52975313

52985314
function preloadPropsFromPreloadOptions(
52995315
href: string,
5300-
as: PreloadAs,
5316+
as: string,
53015317
options: PreloadOptions,
53025318
): PreloadProps {
53035319
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)