Skip to content

Commit 1d0c328

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 4479715 commit 1d0c328

File tree

3 files changed

+84
-117
lines changed

3 files changed

+84
-117
lines changed

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

Lines changed: 33 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ import {ConcurrentMode, NoMode} from 'react-reconciler/src/ReactTypeOfMode';
2525

2626
import hasOwnProperty from 'shared/hasOwnProperty';
2727
import {checkAttributeStringCoercion} from 'shared/CheckStringCoercion';
28-
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
29-
const {Dispatcher} = ReactDOMSharedInternals;
3028

3129
import {
3230
precacheFiberNode,
@@ -1939,24 +1937,9 @@ export function prepareToCommitHoistables() {
19391937
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
19401938
// appropriate for resources that don't really have a strict tie to the document itself for example
19411939
// 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-
}
1940+
export function prepareRendererToRender(rootContainer: Container) {}
19531941

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

19611944
// global collections of Resources
19621945
const preloadPropsMap: Map<string, PreloadProps> = new Map();
@@ -1979,25 +1962,6 @@ function getCurrentResourceRoot(): null | HoistableRoot {
19791962
return currentContainer ? getHoistableRoot(currentContainer) : null;
19801963
}
19811964

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-
20011965
function getDocumentFromRoot(root: HoistableRoot): Document {
20021966
return root.ownerDocument || root;
20031967
}
@@ -2012,13 +1976,23 @@ export const ReactDOMClientDispatcher = {
20121976
preinit,
20131977
};
20141978

1979+
// We expect this to get inlined. It is a function mostly to communicate the special nature of
1980+
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
1981+
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
1982+
// and so we have to fall back to something universal. Currently we just refer to the global document.
1983+
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
1984+
// because we may be rendering inside an iframe.
1985+
function getDocumentForImperativeFloatMethods(): Document {
1986+
return document;
1987+
}
1988+
20151989
function preconnectAs(
20161990
rel: 'preconnect' | 'dns-prefetch',
20171991
crossOrigin: null | '' | 'use-credentials',
20181992
href: string,
20191993
) {
2020-
const ownerDocument = getDocumentForPreloads();
2021-
if (typeof href === 'string' && href && ownerDocument) {
1994+
const ownerDocument = getDocumentForImperativeFloatMethods();
1995+
if (typeof href === 'string' && href) {
20221996
const limitedEscapedHref =
20231997
escapeSelectorAttributeValueInsideDoubleQuotes(href);
20241998
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
@@ -2040,6 +2014,9 @@ function preconnectAs(
20402014
}
20412015
20422016
function prefetchDNS(href: string, options?: mixed) {
2017+
if (!enableFloat) {
2018+
return;
2019+
}
20432020
if (__DEV__) {
20442021
if (typeof href !== 'string' || !href) {
20452022
console.error(
@@ -2067,6 +2044,9 @@ function prefetchDNS(href: string, options?: mixed) {
20672044
}
20682045
20692046
function preconnect(href: string, options?: {crossOrigin?: string}) {
2047+
if (!enableFloat) {
2048+
return;
2049+
}
20702050
if (__DEV__) {
20712051
if (typeof href !== 'string' || !href) {
20722052
console.error(
@@ -2102,10 +2082,13 @@ type PreloadOptions = {
21022082
type?: string,
21032083
};
21042084
function preload(href: string, options: PreloadOptions) {
2085+
if (!enableFloat) {
2086+
return;
2087+
}
21052088
if (__DEV__) {
21062089
validatePreloadArguments(href, options);
21072090
}
2108-
const ownerDocument = getDocumentForPreloads();
2091+
const ownerDocument = getDocumentForImperativeFloatMethods();
21092092
if (
21102093
typeof href === 'string' &&
21112094
href &&
@@ -2163,61 +2146,25 @@ type PreinitOptions = {
21632146
integrity?: string,
21642147
};
21652148
function preinit(href: string, options: PreinitOptions) {
2149+
if (!enableFloat) {
2150+
return;
2151+
}
21662152
if (__DEV__) {
21672153
validatePreinitArguments(href, options);
21682154
}
2155+
const ownerDocument = getDocumentForImperativeFloatMethods();
21692156
21702157
if (
21712158
typeof href === 'string' &&
21722159
href &&
21732160
typeof options === 'object' &&
21742161
options !== null
21752162
) {
2176-
const resourceRoot = getCurrentResourceRoot();
21772163
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-
}
22172164
22182165
switch (as) {
22192166
case 'style': {
2220-
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2167+
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
22212168
22222169
const key = getStyleKey(href);
22232170
const precedence = options.precedence || 'default';
@@ -2236,7 +2183,7 @@ function preinit(href: string, options: PreinitOptions) {
22362183
};
22372184
22382185
// Attempt to hydrate instance from DOM
2239-
let instance: null | Instance = resourceRoot.querySelector(
2186+
let instance: null | Instance = ownerDocument.querySelector(
22402187
getStylesheetSelectorFromKey(key),
22412188
);
22422189
if (instance) {
@@ -2252,7 +2199,6 @@ function preinit(href: string, options: PreinitOptions) {
22522199
if (preloadProps) {
22532200
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
22542201
}
2255-
const ownerDocument = getDocumentFromRoot(resourceRoot);
22562202
const link = (instance = ownerDocument.createElement('link'));
22572203
markNodeAsHoistable(link);
22582204
setInitialProperties(link, 'link', stylesheetProps);
@@ -2269,7 +2215,7 @@ function preinit(href: string, options: PreinitOptions) {
22692215
});
22702216
22712217
state.loading |= Inserted;
2272-
insertStylesheet(instance, precedence, resourceRoot);
2218+
insertStylesheet(instance, precedence, ownerDocument);
22732219
}
22742220
22752221
// Construct a Resource and cache it
@@ -2284,7 +2230,7 @@ function preinit(href: string, options: PreinitOptions) {
22842230
}
22852231
case 'script': {
22862232
const src = href;
2287-
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2233+
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
22882234
22892235
const key = getScriptKey(src);
22902236
@@ -2297,7 +2243,7 @@ function preinit(href: string, options: PreinitOptions) {
22972243
}
22982244
22992245
// Attempt to hydrate instance from DOM
2300-
let instance: null | Instance = resourceRoot.querySelector(
2246+
let instance: null | Instance = ownerDocument.querySelector(
23012247
getScriptSelectorFromKey(key),
23022248
);
23032249
if (!instance) {
@@ -2308,7 +2254,6 @@ function preinit(href: string, options: PreinitOptions) {
23082254
if (preloadProps) {
23092255
adoptPreloadPropsForScript(scriptProps, preloadProps);
23102256
}
2311-
const ownerDocument = getDocumentFromRoot(resourceRoot);
23122257
instance = ownerDocument.createElement('script');
23132258
markNodeAsHoistable(instance);
23142259
setInitialProperties(instance, 'link', scriptProps);
@@ -2329,20 +2274,6 @@ function preinit(href: string, options: PreinitOptions) {
23292274
}
23302275
}
23312276
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-
23462277
function stylesheetPropsFromPreinitOptions(
23472278
href: string,
23482279
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: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -247,11 +247,8 @@ export function createRoot(
247247
transitionCallbacks,
248248
);
249249
markContainerAsRoot(root.current, container);
250+
Dispatcher.current = ReactDOMClientDispatcher;
250251

251-
if (enableFloat) {
252-
// Set the default dispatcher to the client dispatcher
253-
Dispatcher.current = ReactDOMClientDispatcher;
254-
}
255252
const rootContainerElement: Document | Element | DocumentFragment =
256253
container.nodeType === COMMENT_NODE
257254
? (container.parentNode: any)
@@ -339,10 +336,7 @@ export function hydrateRoot(
339336
transitionCallbacks,
340337
);
341338
markContainerAsRoot(root.current, container);
342-
if (enableFloat) {
343-
// Set the default dispatcher to the client dispatcher
344-
Dispatcher.current = ReactDOMClientDispatcher;
345-
}
339+
Dispatcher.current = ReactDOMClientDispatcher;
346340
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
347341
listenToAllSupportedEvents(container);
348342

0 commit comments

Comments
 (0)