Skip to content

Commit

Permalink
Flight support for Float
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff committed Apr 21, 2023
1 parent fd3fb8e commit 0763c04
Show file tree
Hide file tree
Showing 58 changed files with 1,016 additions and 171 deletions.
12 changes: 12 additions & 0 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,14 @@ import type {
SSRManifest,
} from './ReactFlightClientConfig';

import type {Directive} from 'react-server/src/ReactFlightServerConfig';

import {
resolveClientReference,
preloadModule,
requireModule,
parseModel,
dispatchDirective,
} from './ReactFlightClientConfig';

import {knownServerReferences} from './ReactFlightServerReferenceRegistry';
Expand Down Expand Up @@ -778,6 +781,15 @@ export function resolveErrorDev(
}
}

export function resolveDirective(
response: Response,
id: number,
model: UninitializedModel,
): void {
const directive = parseModel<Directive>(response, model);
dispatchDirective(directive);
}

export function close(response: Response): void {
// In case there are any remaining unresolved chunks, they won't
// be resolved now. So we need to issue an error to those.
Expand Down
5 changes: 5 additions & 0 deletions packages/react-client/src/ReactFlightClientStream.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
resolveModel,
resolveErrorProd,
resolveErrorDev,
resolveDirective,
createResponse as createResponseBase,
parseModelString,
parseModelTuple,
Expand Down Expand Up @@ -46,6 +47,10 @@ function processFullRow(response: Response, row: string): void {
resolveModule(response, id, row.slice(colon + 2));
return;
}
case '!': {
resolveDirective(response, id, row.slice(colon + 2));
return;
}
case 'E': {
const errorInfo = JSON.parse(row.slice(colon + 2));
if (__DEV__) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const resolveClientReference = $$$config.resolveClientReference;
export const resolveServerReference = $$$config.resolveServerReference;
export const preloadModule = $$$config.preloadModule;
export const requireModule = $$$config.requireModule;
export const dispatchDirective = $$$config.dispatchDirective;

export opaque type Source = mixed;

Expand Down
20 changes: 9 additions & 11 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* @flow
*/

import type {HostDispatcher} from 'react-dom/src/ReactDOMDispatcher';
import type {EventPriority} from 'react-reconciler/src/ReactEventPriorities';
import type {DOMEventName} from '../events/DOMEventNames';
import type {Fiber, FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
Expand Down Expand Up @@ -1868,10 +1869,6 @@ export function clearSingleton(instance: Instance): void {

export const supportsResources = true;

// The resource types we support. currently they match the form for the as argument.
// In the future this may need to change, especially when modules / scripts are supported
type ResourceType = 'style' | 'font' | 'script';

type HoistableTagType = 'link' | 'meta' | 'title';
type TResource<
T: 'stylesheet' | 'style' | 'script' | 'void',
Expand Down Expand Up @@ -1962,7 +1959,7 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
// We want this to be the default dispatcher on ReactDOMSharedInternals but we don't want to mutate
// internals in Module scope. Instead we export it and Internals will import it. There is already a cycle
// from Internals -> ReactDOM -> HostConfig -> Internals so this doesn't introduce a new one.
export const ReactDOMClientDispatcher = {
export const ReactDOMClientDispatcher: HostDispatcher = {
prefetchDNS,
preconnect,
preload,
Expand Down Expand Up @@ -2036,7 +2033,10 @@ function prefetchDNS(href: string, options?: mixed) {
preconnectAs('dns-prefetch', null, href);
}
function preconnect(href: string, options?: {crossOrigin?: string}) {
function preconnect(href: string, options: ?{crossOrigin?: string}) {
if (!enableFloat) {
return;
}
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand Down Expand Up @@ -2064,9 +2064,8 @@ function preconnect(href: string, options?: {crossOrigin?: string}) {
preconnectAs('preconnect', crossOrigin, href);
}
type PreloadAs = ResourceType;
type PreloadOptions = {
as: PreloadAs,
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
Expand Down Expand Up @@ -2115,7 +2114,7 @@ function preload(href: string, options: PreloadOptions) {
function preloadPropsFromPreloadOptions(
href: string,
as: ResourceType,
as: string,
options: PreloadOptions,
): PreloadProps {
return {
Expand All @@ -2128,9 +2127,8 @@ function preloadPropsFromPreloadOptions(
};
}
type PreinitAs = 'style' | 'script';
type PreinitOptions = {
as: PreinitAs,
as: string,
precedence?: string,
crossOrigin?: string,
integrity?: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ export function scheduleWork(callback: () => void) {

export function flushBuffered(destination: Destination) {}

export const supportsRequestStorage = false;
export const requestStorage: AsyncLocalStorage<any> = (null: any);

export function beginWriting(destination: Destination) {}

export function writeChunk(
Expand Down
117 changes: 57 additions & 60 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ import {
stringToPrecomputedChunk,
clonePrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';
import {
resolveResources,
pingRequest,
} from 'react-server/src/ReactFizzResources';

import isAttributeNameSafe from '../shared/isAttributeNameSafe';
import isUnitlessNumber from '../shared/isUnitlessNumber';
Expand Down Expand Up @@ -79,30 +83,15 @@ import {
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;

const ReactDOMServerDispatcher = enableFloat
? {
prefetchDNS,
preconnect,
preload,
preinit,
}
: {};

let currentResources: null | Resources = null;
const currentResourcesStack = [];

export function prepareToRender(resources: Resources): mixed {
currentResourcesStack.push(currentResources);
currentResources = resources;
const ReactDOMServerDispatcher = {
prefetchDNS,
preconnect,
preload,
preinit,
};

const previousHostDispatcher = ReactDOMCurrentDispatcher.current;
export function prepareHostDispatcher() {
ReactDOMCurrentDispatcher.current = ReactDOMServerDispatcher;
return previousHostDispatcher;
}

export function cleanupAfterRender(previousDispatcher: mixed) {
currentResources = currentResourcesStack.pop();
ReactDOMCurrentDispatcher.current = previousDispatcher;
}

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

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

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

type PreloadAs = 'style' | 'font' | 'script';
type PreloadOptions = {
as: PreloadAs,
as: string,
crossOrigin?: string,
integrity?: string,
type?: string,
};
export function preload(href: string, options: PreloadOptions) {
if (!currentResources) {
// While we expect that preload calls are primarily going to be observed
// during render because effects and events don't run on the server it is
// still possible that these get called in module scope. This is valid on
// the client since there is still a document to interact with but on the
// server we need a request to associate the call to. Because of this we
// simply return and do not warn.
if (!enableFloat) {
return;
}
const resources = resolveResources();
if (!resources) {
// In async contexts we can sometimes resolve resources from AsyncLocalStorage. If we can't we can also
// possibly get them from the stack if we are not in an async context. Since we were not able to resolve
// the resources for this call in either case we opt to do nothing. We can consider making this a warning
// but there may be times where calling a function outside of render is intentional (i.e. to warm up data
// fetching) and we don't want to warn in those cases.
return;
}
const resources = currentResources;
if (__DEV__) {
if (typeof href !== 'string' || !href) {
console.error(
Expand Down Expand Up @@ -5055,27 +5049,30 @@ export function preload(href: string, options: PreloadOptions) {
resources.explicitOtherPreloads.add(resource);
}
}
pingRequest();
}
}

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

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

function preloadPropsFromPreloadOptions(
href: string,
as: PreloadAs,
as: string,
options: PreloadOptions,
): PreloadProps {
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,7 @@ export {
writePostamble,
hoistResources,
setCurrentlyRenderingBoundaryResourcesTarget,
prepareToRender,
cleanupAfterRender,
prepareHostDispatcher,
} from './ReactFizzConfigDOM';

import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
Expand Down
Loading

0 comments on commit 0763c04

Please sign in to comment.