Skip to content

Commit e25fbfb

Browse files
committed
Allow Float methods to be called anytime in Fiber
Previously we restricted Float methods to only being callable while rendering. This allowed us to make associations between calls and their position in the DOM tree, for instance hoisting preinitialized styles into a ShadowRoot or an iframe Document. When considering how we are going to support Flight support in Float however it became clear that this restriction would lead to compromises on the implementation because the Flight client does not execute within the context of a client render. We want to be able to disaptch Float directives coming from Flight as soon as possible and this requires being able to call them outside of render. this patch modifies Float so that its methods are callable anywhere. The main consequence of this change is Float will always use the Document the renderer script is running within as the HoistableRoot. This means if you preinit as style inside a component render targeting a ShadowRoot the style will load in the ownerDocument not the ShadowRoot. Practially speaking it means that preinit is not useful inside ShadowRoots and iframes. This tradeoff was deemed acceptable because these methods are optimistic, not critical. Additionally, the other methods, preconntect, prefetchDNS, and preload, are not impacted because they already operated at the level of the ownerDocument and really only interface with the Network cache layer.
1 parent 8e74775 commit e25fbfb

File tree

3 files changed

+79
-118
lines changed

3 files changed

+79
-118
lines changed

packages/react-dom-bindings/src/client/ReactDOMHostConfig.js

Lines changed: 27 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1939,24 +1939,9 @@ export function prepareToCommitHoistables() {
19391939
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
19401940
// appropriate for resources that don't really have a strict tie to the document itself for example
19411941
// preloads
1942-
let lastCurrentDocument: ?Document = null;
1943-
let previousDispatcher = null;
1944-
export function prepareRendererToRender(rootContainer: Container) {
1945-
if (enableFloat) {
1946-
const rootNode = getHoistableRoot(rootContainer);
1947-
lastCurrentDocument = getDocumentFromRoot(rootNode);
1948-
1949-
previousDispatcher = Dispatcher.current;
1950-
Dispatcher.current = ReactDOMClientDispatcher;
1951-
}
1952-
}
1942+
export function prepareRendererToRender(rootContainer: Container) {}
19531943

1954-
export function resetRendererAfterRender() {
1955-
if (enableFloat) {
1956-
Dispatcher.current = previousDispatcher;
1957-
previousDispatcher = null;
1958-
}
1959-
}
1944+
export function resetRendererAfterRender() {}
19601945

19611946
// global collections of Resources
19621947
const preloadPropsMap: Map<string, PreloadProps> = new Map();
@@ -1979,25 +1964,6 @@ function getCurrentResourceRoot(): null | HoistableRoot {
19791964
return currentContainer ? getHoistableRoot(currentContainer) : null;
19801965
}
19811966

1982-
// Preloads are somewhat special. Even if we don't have the Document
1983-
// used by the root that is rendering a component trying to insert a preload
1984-
// we can still seed the file cache by doing the preload on any document we have
1985-
// access to. We prefer the currentDocument if it exists, we also prefer the
1986-
// lastCurrentDocument if that exists. As a fallback we will use the window.document
1987-
// if available.
1988-
function getDocumentForPreloads(): ?Document {
1989-
const root = getCurrentResourceRoot();
1990-
if (root) {
1991-
return root.ownerDocument || root;
1992-
} else {
1993-
try {
1994-
return lastCurrentDocument || window.document;
1995-
} catch (error) {
1996-
return null;
1997-
}
1998-
}
1999-
}
2000-
20011967
function getDocumentFromRoot(root: HoistableRoot): Document {
20021968
return root.ownerDocument || root;
20031969
}
@@ -2012,13 +1978,17 @@ export const ReactDOMClientDispatcher = {
20121978
preinit,
20131979
};
20141980

1981+
export function prepareHostDispatcher() {
1982+
Dispatcher.current = ReactDOMClientDispatcher;
1983+
}
1984+
20151985
function preconnectAs(
20161986
rel: 'preconnect' | 'dns-prefetch',
20171987
crossOrigin: null | '' | 'use-credentials',
20181988
href: string,
20191989
) {
2020-
const ownerDocument = getDocumentForPreloads();
2021-
if (typeof href === 'string' && href && ownerDocument) {
1990+
const ownerDocument = document;
1991+
if (typeof href === 'string' && href) {
20221992
const limitedEscapedHref =
20231993
escapeSelectorAttributeValueInsideDoubleQuotes(href);
20241994
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
@@ -2040,6 +2010,9 @@ function preconnectAs(
20402010
}
20412011

20422012
function prefetchDNS(href: string, options?: mixed) {
2013+
if (!enableFloat) {
2014+
return;
2015+
}
20432016
if (__DEV__) {
20442017
if (typeof href !== 'string' || !href) {
20452018
console.error(
@@ -2067,6 +2040,9 @@ function prefetchDNS(href: string, options?: mixed) {
20672040
}
20682041

20692042
function preconnect(href: string, options?: {crossOrigin?: string}) {
2043+
if (!enableFloat) {
2044+
return;
2045+
}
20702046
if (__DEV__) {
20712047
if (typeof href !== 'string' || !href) {
20722048
console.error(
@@ -2102,10 +2078,13 @@ type PreloadOptions = {
21022078
type?: string,
21032079
};
21042080
function preload(href: string, options: PreloadOptions) {
2081+
if (!enableFloat) {
2082+
return;
2083+
}
21052084
if (__DEV__) {
21062085
validatePreloadArguments(href, options);
21072086
}
2108-
const ownerDocument = getDocumentForPreloads();
2087+
const ownerDocument = document;
21092088
if (
21102089
typeof href === 'string' &&
21112090
href &&
@@ -2163,61 +2142,25 @@ type PreinitOptions = {
21632142
integrity?: string,
21642143
};
21652144
function preinit(href: string, options: PreinitOptions) {
2145+
if (!enableFloat) {
2146+
return;
2147+
}
21662148
if (__DEV__) {
21672149
validatePreinitArguments(href, options);
21682150
}
2151+
const ownerDocument = document;
21692152

21702153
if (
21712154
typeof href === 'string' &&
21722155
href &&
21732156
typeof options === 'object' &&
21742157
options !== null
21752158
) {
2176-
const resourceRoot = getCurrentResourceRoot();
21772159
const as = options.as;
2178-
if (!resourceRoot) {
2179-
if (as === 'style' || as === 'script') {
2180-
// We are going to emit a preload as a best effort fallback since this preinit
2181-
// was called outside of a render. Given the passive nature of this fallback
2182-
// we do not warn in dev when props disagree if there happens to already be a
2183-
// matching preload with this href
2184-
const preloadDocument = getDocumentForPreloads();
2185-
if (preloadDocument) {
2186-
const limitedEscapedHref =
2187-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
2188-
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2189-
let key = preloadKey;
2190-
switch (as) {
2191-
case 'style':
2192-
key = getStyleKey(href);
2193-
break;
2194-
case 'script':
2195-
key = getScriptKey(href);
2196-
break;
2197-
}
2198-
if (!preloadPropsMap.has(key)) {
2199-
const preloadProps = preloadPropsFromPreinitOptions(
2200-
href,
2201-
as,
2202-
options,
2203-
);
2204-
preloadPropsMap.set(key, preloadProps);
2205-
2206-
if (null === preloadDocument.querySelector(preloadKey)) {
2207-
const instance = preloadDocument.createElement('link');
2208-
setInitialProperties(instance, 'link', preloadProps);
2209-
markNodeAsHoistable(instance);
2210-
(preloadDocument.head: any).appendChild(instance);
2211-
}
2212-
}
2213-
}
2214-
}
2215-
return;
2216-
}
22172160

22182161
switch (as) {
22192162
case 'style': {
2220-
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2163+
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
22212164

22222165
const key = getStyleKey(href);
22232166
const precedence = options.precedence || 'default';
@@ -2236,7 +2179,7 @@ function preinit(href: string, options: PreinitOptions) {
22362179
};
22372180

22382181
// Attempt to hydrate instance from DOM
2239-
let instance: null | Instance = resourceRoot.querySelector(
2182+
let instance: null | Instance = ownerDocument.querySelector(
22402183
getStylesheetSelectorFromKey(key),
22412184
);
22422185
if (instance) {
@@ -2252,7 +2195,6 @@ function preinit(href: string, options: PreinitOptions) {
22522195
if (preloadProps) {
22532196
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
22542197
}
2255-
const ownerDocument = getDocumentFromRoot(resourceRoot);
22562198
const link = (instance = ownerDocument.createElement('link'));
22572199
markNodeAsHoistable(link);
22582200
setInitialProperties(link, 'link', stylesheetProps);
@@ -2269,7 +2211,7 @@ function preinit(href: string, options: PreinitOptions) {
22692211
});
22702212

22712213
state.loading |= Inserted;
2272-
insertStylesheet(instance, precedence, resourceRoot);
2214+
insertStylesheet(instance, precedence, ownerDocument);
22732215
}
22742216

22752217
// Construct a Resource and cache it
@@ -2284,7 +2226,7 @@ function preinit(href: string, options: PreinitOptions) {
22842226
}
22852227
case 'script': {
22862228
const src = href;
2287-
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2229+
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
22882230

22892231
const key = getScriptKey(src);
22902232

@@ -2297,7 +2239,7 @@ function preinit(href: string, options: PreinitOptions) {
22972239
}
22982240

22992241
// Attempt to hydrate instance from DOM
2300-
let instance: null | Instance = resourceRoot.querySelector(
2242+
let instance: null | Instance = ownerDocument.querySelector(
23012243
getScriptSelectorFromKey(key),
23022244
);
23032245
if (!instance) {
@@ -2308,7 +2250,6 @@ function preinit(href: string, options: PreinitOptions) {
23082250
if (preloadProps) {
23092251
adoptPreloadPropsForScript(scriptProps, preloadProps);
23102252
}
2311-
const ownerDocument = getDocumentFromRoot(resourceRoot);
23122253
instance = ownerDocument.createElement('script');
23132254
markNodeAsHoistable(instance);
23142255
setInitialProperties(instance, 'link', scriptProps);
@@ -2329,20 +2270,6 @@ function preinit(href: string, options: PreinitOptions) {
23292270
}
23302271
}
23312272

2332-
function preloadPropsFromPreinitOptions(
2333-
href: string,
2334-
as: ResourceType,
2335-
options: PreinitOptions,
2336-
): PreloadProps {
2337-
return {
2338-
href,
2339-
rel: 'preload',
2340-
as,
2341-
crossOrigin: as === 'font' ? '' : options.crossOrigin,
2342-
integrity: options.integrity,
2343-
};
2344-
}
2345-
23462273
function stylesheetPropsFromPreinitOptions(
23472274
href: string,
23482275
precedence: string,

packages/react-dom/src/__tests__/ReactDOMFloat-test.js

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ let ReactDOMFizzServer;
2424
let Suspense;
2525
let textCache;
2626
let loadCache;
27-
let window;
28-
let document;
2927
let writable;
3028
const CSPnonce = null;
3129
let container;
@@ -51,8 +49,8 @@ function resetJSDOM(markup) {
5149
media: query,
5250
})),
5351
});
54-
window = jsdom.window;
55-
document = jsdom.window.document;
52+
global.window = jsdom.window;
53+
global.document = jsdom.window.document;
5654
}
5755

5856
describe('ReactDOMFloat', () => {
@@ -3750,7 +3748,7 @@ body {
37503748
});
37513749

37523750
// @gate enableFloat
3753-
it('creates a preload resource when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
3751+
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
37543752
function App() {
37553753
React.useEffect(() => {
37563754
ReactDOM.preinit('foo', {as: 'style'});
@@ -3768,11 +3766,55 @@ body {
37683766
expect(getMeaningfulChildren(document)).toEqual(
37693767
<html>
37703768
<head>
3771-
<link rel="preload" href="foo" as="style" />
3769+
<link rel="stylesheet" href="foo" data-precedence="default" />
3770+
</head>
3771+
<body>foo</body>
3772+
</html>,
3773+
);
3774+
});
3775+
3776+
// @gate enableFloat
3777+
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
3778+
// This is testing behavior, but it shows that it is not a good idea to preinit inside a shadowRoot. The point is we are asserting a behavior
3779+
// you would want to avoid in a real app.
3780+
const shadow = document.body.attachShadow({mode: 'open'});
3781+
function ShadowComponent() {
3782+
ReactDOM.preinit('bar', {as: 'style'});
3783+
return null;
3784+
}
3785+
function App() {
3786+
React.useEffect(() => {
3787+
ReactDOM.preinit('foo', {as: 'style'});
3788+
}, []);
3789+
return (
3790+
<html>
3791+
<body>
3792+
foo
3793+
{ReactDOM.createPortal(
3794+
<div>
3795+
<ShadowComponent />
3796+
shadow
3797+
</div>,
3798+
shadow,
3799+
)}
3800+
</body>
3801+
</html>
3802+
);
3803+
}
3804+
3805+
const root = ReactDOMClient.createRoot(document);
3806+
root.render(<App />);
3807+
await waitForAll([]);
3808+
expect(getMeaningfulChildren(document)).toEqual(
3809+
<html>
3810+
<head>
3811+
<link rel="stylesheet" href="bar" data-precedence="default" />
3812+
<link rel="stylesheet" href="foo" data-precedence="default" />
37723813
</head>
37733814
<body>foo</body>
37743815
</html>,
37753816
);
3817+
expect(getMeaningfulChildren(shadow)).toEqual(<div>shadow</div>);
37763818
});
37773819

37783820
// @gate enableFloat
@@ -3872,7 +3914,7 @@ body {
38723914
expect(getMeaningfulChildren(document)).toEqual(
38733915
<html>
38743916
<head>
3875-
<link rel="preload" href="foo" as="script" />
3917+
<script async="" src="foo" />
38763918
</head>
38773919
<body>foo</body>
38783920
</html>,

packages/react-dom/src/client/ReactDOMRoot.js

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import type {
1313
TransitionTracingCallbacks,
1414
} from 'react-reconciler/src/ReactInternalTypes';
1515

16-
import ReactDOMSharedInternals from '../ReactDOMSharedInternals';
17-
const {Dispatcher} = ReactDOMSharedInternals;
18-
import {ReactDOMClientDispatcher} from 'react-dom-bindings/src/client/ReactDOMHostConfig';
16+
import {prepareHostDispatcher} from 'react-dom-bindings/src/client/ReactDOMHostConfig';
1917
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
2018
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';
2119
import {
@@ -247,11 +245,8 @@ export function createRoot(
247245
transitionCallbacks,
248246
);
249247
markContainerAsRoot(root.current, container);
248+
prepareHostDispatcher();
250249

251-
if (enableFloat) {
252-
// Set the default dispatcher to the client dispatcher
253-
Dispatcher.current = ReactDOMClientDispatcher;
254-
}
255250
const rootContainerElement: Document | Element | DocumentFragment =
256251
container.nodeType === COMMENT_NODE
257252
? (container.parentNode: any)
@@ -339,10 +334,7 @@ export function hydrateRoot(
339334
transitionCallbacks,
340335
);
341336
markContainerAsRoot(root.current, container);
342-
if (enableFloat) {
343-
// Set the default dispatcher to the client dispatcher
344-
Dispatcher.current = ReactDOMClientDispatcher;
345-
}
337+
prepareHostDispatcher();
346338
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
347339
listenToAllSupportedEvents(container);
348340

0 commit comments

Comments
 (0)