Skip to content

Commit 0763c04

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 0763c04

File tree

58 files changed

+1016
-171
lines changed

Some content is hidden

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

58 files changed

+1016
-171
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 {Directive} from 'react-server/src/ReactFlightServerConfig';
22+
2123
import {
2224
resolveClientReference,
2325
preloadModule,
2426
requireModule,
2527
parseModel,
28+
dispatchDirective,
2629
} from './ReactFlightClientConfig';
2730

2831
import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
@@ -778,6 +781,15 @@ export function resolveErrorDev(
778781
}
779782
}
780783

784+
export function resolveDirective(
785+
response: Response,
786+
id: number,
787+
model: UninitializedModel,
788+
): void {
789+
const directive = parseModel<Directive>(response, model);
790+
dispatchDirective(directive);
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: 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 '!': {
51+
resolveDirective(response, id, row.slice(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: 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: 57 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ import {
3838
stringToPrecomputedChunk,
3939
clonePrecomputedChunk,
4040
} from 'react-server/src/ReactServerStreamConfig';
41+
import {
42+
resolveResources,
43+
pingRequest,
44+
} from 'react-server/src/ReactFizzResources';
4145

4246
import isAttributeNameSafe from '../shared/isAttributeNameSafe';
4347
import isUnitlessNumber from '../shared/isUnitlessNumber';
@@ -79,30 +83,15 @@ import {
7983
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
8084
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
8185

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;
86+
const ReactDOMServerDispatcher = {
87+
prefetchDNS,
88+
preconnect,
89+
preload,
90+
preinit,
91+
};
9792

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

10897
// Used to distinguish these contexts from ones used in other renderers.
@@ -4804,16 +4793,18 @@ function getResourceKey(as: string, href: string): string {
48044793
}
48054794

48064795
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.
4796+
if (!enableFloat) {
4797+
return;
4798+
}
4799+
const resources = resolveResources();
4800+
if (!resources) {
4801+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4802+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4803+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4804+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4805+
// fetching) and we don't want to warn in those cases.
48144806
return;
48154807
}
4816-
const resources = currentResources;
48174808
if (__DEV__) {
48184809
if (typeof href !== 'string' || !href) {
48194810
console.error(
@@ -4858,17 +4849,19 @@ export function prefetchDNS(href: string, options?: mixed) {
48584849
}
48594850
}
48604851

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.
4852+
export function preconnect(href: string, options?: ?{crossOrigin?: string}) {
4853+
if (!enableFloat) {
4854+
return;
4855+
}
4856+
const resources = resolveResources();
4857+
if (!resources) {
4858+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4859+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4860+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4861+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4862+
// fetching) and we don't want to warn in those cases.
48694863
return;
48704864
}
4871-
const resources = currentResources;
48724865
if (__DEV__) {
48734866
if (typeof href !== 'string' || !href) {
48744867
console.error(
@@ -4917,24 +4910,25 @@ export function preconnect(href: string, options?: {crossOrigin?: string}) {
49174910
}
49184911
}
49194912

4920-
type PreloadAs = 'style' | 'font' | 'script';
49214913
type PreloadOptions = {
4922-
as: PreloadAs,
4914+
as: string,
49234915
crossOrigin?: string,
49244916
integrity?: string,
49254917
type?: string,
49264918
};
49274919
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.
4920+
if (!enableFloat) {
4921+
return;
4922+
}
4923+
const resources = resolveResources();
4924+
if (!resources) {
4925+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
4926+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
4927+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
4928+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
4929+
// fetching) and we don't want to warn in those cases.
49354930
return;
49364931
}
4937-
const resources = currentResources;
49384932
if (__DEV__) {
49394933
if (typeof href !== 'string' || !href) {
49404934
console.error(
@@ -5055,27 +5049,30 @@ export function preload(href: string, options: PreloadOptions) {
50555049
resources.explicitOtherPreloads.add(resource);
50565050
}
50575051
}
5052+
pingRequest();
50585053
}
50595054
}
50605055

5061-
type PreinitAs = 'style' | 'script';
50625056
type PreinitOptions = {
5063-
as: PreinitAs,
5057+
as: string,
50645058
precedence?: string,
50655059
crossOrigin?: string,
50665060
integrity?: string,
50675061
};
50685062
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.
5063+
if (!enableFloat) {
5064+
return;
5065+
}
5066+
const resources = resolveResources();
5067+
if (!resources) {
5068+
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
5069+
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
5070+
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
5071+
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
5072+
// fetching) and we don't want to warn in those cases.
50765073
return;
50775074
}
5078-
preinitImpl(currentResources, href, options);
5075+
preinitImpl(resources, href, options);
50795076
}
50805077

50815078
// On the server, preinit may be called outside of render when sending an
@@ -5297,7 +5294,7 @@ function preinitImpl(
52975294

52985295
function preloadPropsFromPreloadOptions(
52995296
href: string,
5300-
as: PreloadAs,
5297+
as: string,
53015298
options: PreloadOptions,
53025299
): PreloadProps {
53035300
return {

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ export {
137137
writePostamble,
138138
hoistResources,
139139
setCurrentlyRenderingBoundaryResourcesTarget,
140-
prepareToRender,
141-
cleanupAfterRender,
140+
prepareHostDispatcher,
142141
} from './ReactFizzConfigDOM';
143142

144143
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';

0 commit comments

Comments
 (0)