Skip to content

Commit

Permalink
[Flight][Fizz][Fiber] Chain HostDispatcher implementations (#28488)
Browse files Browse the repository at this point in the history
The idea here is that host dispatchers are not bound to renders so we
need to be able to dispatch to them at any time. This updates the
implementation to chain these dispatchers so that each renderer can
respond to the dispatch. Semantically we don't always want every
renderer to do this for instance if Fizz handles a float method we don't
want Fiber to as well so each dispatcher implementation can decide if it
makes sense to forward the call or not. For float methods server
disaptchers will handle the call if they can resolve a Request otherwise
they will forward. For client dispatchers they will handle the call and
always forward. The choice needs to be made for each dispatcher method
and may have implications on correct renderer import order. For now we
just live with the restriction that if you want to use server and client
together (such as renderToString in the browser) you need to import the
server renderer after the client renderer.
  • Loading branch information
gnoff authored Mar 4, 2024
1 parent 1c02b9d commit 113ab9a
Show file tree
Hide file tree
Showing 20 changed files with 202 additions and 186 deletions.
47 changes: 28 additions & 19 deletions packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
* @flow
*/

import type {HostDispatcher} from 'react-dom/src/shared/ReactDOMTypes';
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 @@ -107,6 +106,10 @@ import {listenToAllSupportedEvents} from '../events/DOMPluginEventSystem';
import {validateLinkPropsForStyleResource} from '../shared/ReactDOMResourceValidation';
import escapeSelectorAttributeValueInsideDoubleQuotes from './escapeSelectorAttributeValueInsideDoubleQuotes';

import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher =
ReactDOMSharedInternals.ReactDOMCurrentDispatcher;

export type Type = string;
export type Props = {
autoFocus?: boolean,
Expand Down Expand Up @@ -2108,10 +2111,8 @@ function getDocumentFromRoot(root: HoistableRoot): Document {
return root.ownerDocument || root;
}

// 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: HostDispatcher = {
const previousDispatcher = ReactDOMCurrentDispatcher.current;
ReactDOMCurrentDispatcher.current = {
prefetchDNS,
preconnect,
preload,
Expand All @@ -2127,17 +2128,18 @@ export const ReactDOMClientDispatcher: HostDispatcher = {
// and so we have to fall back to something universal. Currently we just refer to the global document.
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
// because we may be rendering inside an iframe.
function getDocumentForImperativeFloatMethods(): Document {
return document;
const globalDocument = typeof document === 'undefined' ? null : document;
function getGlobalDocument(): ?Document {
return globalDocument;
}

function preconnectAs(
rel: 'preconnect' | 'dns-prefetch',
href: string,
crossOrigin: ?CrossOriginEnum,
) {
const ownerDocument = getDocumentForImperativeFloatMethods();
if (typeof href === 'string' && href) {
const ownerDocument = getGlobalDocument();
if (ownerDocument && typeof href === 'string' && href) {
const limitedEscapedHref =
escapeSelectorAttributeValueInsideDoubleQuotes(href);
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
Expand All @@ -2162,22 +2164,25 @@ function prefetchDNS(href: string) {
if (!enableFloat) {
return;
}
previousDispatcher.prefetchDNS(href);
preconnectAs('dns-prefetch', href, null);
}

function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) {
if (!enableFloat) {
return;
}
previousDispatcher.preconnect(href, crossOrigin);
preconnectAs('preconnect', href, crossOrigin);
}

function preload(href: string, as: string, options?: ?PreloadImplOptions) {
if (!enableFloat) {
return;
}
const ownerDocument = getDocumentForImperativeFloatMethods();
if (href && as && ownerDocument) {
previousDispatcher.preload(href, as, options);
const ownerDocument = getGlobalDocument();
if (ownerDocument && href && as) {
let preloadSelector = `link[rel="preload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
as,
)}"]`;
Expand Down Expand Up @@ -2256,8 +2261,9 @@ function preloadModule(href: string, options?: ?PreloadModuleImplOptions) {
if (!enableFloat) {
return;
}
const ownerDocument = getDocumentForImperativeFloatMethods();
if (href) {
previousDispatcher.preloadModule(href, options);
const ownerDocument = getGlobalDocument();
if (ownerDocument && href) {
const as =
options && typeof options.as === 'string' ? options.as : 'script';
const preloadSelector = `link[rel="modulepreload"][as="${escapeSelectorAttributeValueInsideDoubleQuotes(
Expand Down Expand Up @@ -2319,9 +2325,10 @@ function preinitStyle(
if (!enableFloat) {
return;
}
const ownerDocument = getDocumentForImperativeFloatMethods();
previousDispatcher.preinitStyle(href, precedence, options);

if (href) {
const ownerDocument = getGlobalDocument();
if (ownerDocument && href) {
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;

const key = getStyleKey(href);
Expand Down Expand Up @@ -2395,9 +2402,10 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions) {
if (!enableFloat) {
return;
}
const ownerDocument = getDocumentForImperativeFloatMethods();
previousDispatcher.preinitScript(src, options);

if (src) {
const ownerDocument = getGlobalDocument();
if (ownerDocument && src) {
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);
Expand Down Expand Up @@ -2453,9 +2461,10 @@ function preinitModuleScript(
if (!enableFloat) {
return;
}
const ownerDocument = getDocumentForImperativeFloatMethods();
previousDispatcher.preinitModuleScript(src, options);

if (src) {
const ownerDocument = getGlobalDocument();
if (ownerDocument && src) {
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;

const key = getScriptKey(src);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import type {
HostDispatcher,
CrossOriginEnum,
PreloadImplOptions,
PreloadModuleImplOptions,
Expand All @@ -25,7 +24,12 @@ import {
resolveRequest,
} from 'react-server/src/ReactFlightServer';

export const ReactDOMFlightServerDispatcher: HostDispatcher = {
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher =
ReactDOMSharedInternals.ReactDOMCurrentDispatcher;

const previousDispatcher = ReactDOMCurrentDispatcher.current;
ReactDOMCurrentDispatcher.current = {
prefetchDNS,
preconnect,
preload,
Expand All @@ -48,6 +52,8 @@ function prefetchDNS(href: string) {
}
hints.add(key);
emitHint(request, 'D', href);
} else {
previousDispatcher.prefetchDNS(href);
}
}
}
Expand All @@ -71,6 +77,8 @@ function preconnect(href: string, crossOrigin?: ?CrossOriginEnum) {
} else {
emitHint(request, 'C', href);
}
} else {
previousDispatcher.preconnect(href, crossOrigin);
}
}
}
Expand Down Expand Up @@ -104,6 +112,8 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
} else {
emitHint(request, 'L', [href, as]);
}
} else {
previousDispatcher.preload(href, as, options);
}
}
}
Expand All @@ -128,6 +138,8 @@ function preloadModule(href: string, options?: ?PreloadModuleImplOptions) {
} else {
return emitHint(request, 'm', href);
}
} else {
previousDispatcher.preloadModule(href, options);
}
}
}
Expand Down Expand Up @@ -162,18 +174,20 @@ function preinitStyle(
} else {
return emitHint(request, 'S', href);
}
} else {
previousDispatcher.preinitStyle(href, precedence, options);
}
}
}
}

function preinitScript(href: string, options?: ?PreinitScriptOptions) {
function preinitScript(src: string, options?: ?PreinitScriptOptions) {
if (enableFloat) {
if (typeof href === 'string') {
if (typeof src === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'X|' + href;
const key = 'X|' + src;
if (hints.has(key)) {
// duplicate hint
return;
Expand All @@ -182,25 +196,27 @@ function preinitScript(href: string, options?: ?PreinitScriptOptions) {

const trimmed = trimOptions(options);
if (trimmed) {
return emitHint(request, 'X', [href, trimmed]);
return emitHint(request, 'X', [src, trimmed]);
} else {
return emitHint(request, 'X', href);
return emitHint(request, 'X', src);
}
} else {
previousDispatcher.preinitScript(src, options);
}
}
}
}

function preinitModuleScript(
href: string,
src: string,
options?: ?PreinitModuleScriptOptions,
) {
if (enableFloat) {
if (typeof href === 'string') {
if (typeof src === 'string') {
const request = resolveRequest();
if (request) {
const hints = getHints(request);
const key = 'M|' + href;
const key = 'M|' + src;
if (hints.has(key)) {
// duplicate hint
return;
Expand All @@ -209,10 +225,12 @@ function preinitModuleScript(

const trimmed = trimOptions(options);
if (trimmed) {
return emitHint(request, 'M', [href, trimmed]);
return emitHint(request, 'M', [src, trimmed]);
} else {
return emitHint(request, 'M', href);
return emitHint(request, 'M', src);
}
} else {
previousDispatcher.preinitModuleScript(src, options);
}
}
}
Expand Down
19 changes: 12 additions & 7 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,20 @@ import {getValueDescriptorExpectingObjectForWarning} from '../shared/ReactDOMRes
import {NotPending} from '../shared/ReactDOMFormActions';

import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;
const ReactDOMCurrentDispatcher =
ReactDOMSharedInternals.ReactDOMCurrentDispatcher;

const ReactDOMServerDispatcher = {
const previousDispatcher = ReactDOMCurrentDispatcher.current;
ReactDOMCurrentDispatcher.current = {
prefetchDNS,
preconnect,
preload,
preloadModule,
preinitStyle,
preinitScript,
preinitStyle,
preinitModuleScript,
};

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

// We make every property of the descriptor optional because it is not a contract that
// the headers provided by onHeaders has any particular header types.
export type HeadersDescriptor = {
Expand Down Expand Up @@ -5342,6 +5340,7 @@ function prefetchDNS(href: string) {
// 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.
previousDispatcher.prefetchDNS(href);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5397,6 +5396,7 @@ function preconnect(href: string, crossOrigin: ?CrossOriginEnum) {
// 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.
previousDispatcher.preconnect(href, crossOrigin);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5460,6 +5460,7 @@ function preload(href: string, as: string, options?: ?PreloadImplOptions) {
// 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.
previousDispatcher.preload(href, as, options);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5663,6 +5664,7 @@ function preloadModule(
// 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.
previousDispatcher.preloadModule(href, options);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5739,6 +5741,7 @@ function preinitStyle(
// 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.
previousDispatcher.preinitStyle(href, precedence, options);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5826,6 +5829,7 @@ function preinitScript(src: string, options?: ?PreinitScriptOptions): void {
// 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.
previousDispatcher.preinitScript(src, options);
return;
}
const resumableState = getResumableState(request);
Expand Down Expand Up @@ -5891,6 +5895,7 @@ function preinitModuleScript(
// 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.
previousDispatcher.preinitModuleScript(src, options);
return;
}
const resumableState = getResumableState(request);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,6 @@ export {
writeHoistables,
writePostamble,
hoistHoistables,
prepareHostDispatcher,
resetResumableState,
completeResumableState,
emitEarlyPreloads,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,9 @@ import type {
PreinitModuleScriptOptions,
} from 'react-dom/src/shared/ReactDOMTypes';

import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals';
const ReactDOMCurrentDispatcher = ReactDOMSharedInternals.Dispatcher;

import {ReactDOMFlightServerDispatcher} from './ReactDOMFlightServerHostDispatcher';

export function prepareHostDispatcher(): void {
ReactDOMCurrentDispatcher.current = ReactDOMFlightServerDispatcher;
}
// This module registers the host dispatcher so it needs to be imported
// but it does not have any exports
import './ReactDOMFlightServerHostDispatcher';

// Used to distinguish these contexts from ones used in other renderers.
// E.g. this can be used to distinguish legacy renderers from this modern one.
Expand Down
Loading

0 comments on commit 113ab9a

Please sign in to comment.