Skip to content

Commit fdad813

Browse files
authored
[Float][Fiber] Enable Float methods to be called outside of render (#26557)
Stacked on #26570 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. I added a couple additional fixes that were necessary for getting tests to pass that are worth considering separately. The first commit improves the diff for `waitForThrow` so it compares strings if possible. The second commit makes invokeGuardedCallback not use metaprogramming pattern and swallows any novel errors produced from trying to run the guarded callback. Swallowing may not be the best we can do but it at least protects React against rapid failure when something causes the dispatchEvent to throw.
1 parent e5708b3 commit fdad813

File tree

11 files changed

+77
-164
lines changed

11 files changed

+77
-164
lines changed

packages/react-art/src/ReactFiberConfigART.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,3 @@ export function suspendInstance(type, props) {}
479479
export function waitForCommitToBeReady() {
480480
return null;
481481
}
482-
// eslint-disable-next-line no-undef
483-
export function prepareRendererToRender(container: Container): void {
484-
// noop
485-
}
486-
487-
export function resetRendererAfterRender(): void {
488-
// noop
489-
}

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

Lines changed: 28 additions & 110 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,
@@ -1936,31 +1934,6 @@ export function prepareToCommitHoistables() {
19361934
tagCaches = null;
19371935
}
19381936

1939-
// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
1940-
// called when there is no rendering we track the last used document. It is not safe to insert
1941-
// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
1942-
// that the resource is meant to apply too (for example stylesheets or scripts). This is only
1943-
// appropriate for resources that don't really have a strict tie to the document itself for example
1944-
// preloads
1945-
let lastCurrentDocument: ?Document = null;
1946-
let previousDispatcher = null;
1947-
export function prepareRendererToRender(rootContainer: Container) {
1948-
if (enableFloat) {
1949-
const rootNode = getHoistableRoot(rootContainer);
1950-
lastCurrentDocument = getDocumentFromRoot(rootNode);
1951-
1952-
previousDispatcher = Dispatcher.current;
1953-
Dispatcher.current = ReactDOMClientDispatcher;
1954-
}
1955-
}
1956-
1957-
export function resetRendererAfterRender() {
1958-
if (enableFloat) {
1959-
Dispatcher.current = previousDispatcher;
1960-
previousDispatcher = null;
1961-
}
1962-
}
1963-
19641937
// global collections of Resources
19651938
const preloadPropsMap: Map<string, PreloadProps> = new Map();
19661939
const preconnectsSet: Set<string> = new Set();
@@ -1982,25 +1955,6 @@ function getCurrentResourceRoot(): null | HoistableRoot {
19821955
return currentContainer ? getHoistableRoot(currentContainer) : null;
19831956
}
19841957

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

1972+
// We expect this to get inlined. It is a function mostly to communicate the special nature of
1973+
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
1974+
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
1975+
// and so we have to fall back to something universal. Currently we just refer to the global document.
1976+
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
1977+
// because we may be rendering inside an iframe.
1978+
function getDocumentForImperativeFloatMethods(): Document {
1979+
return document;
1980+
}
1981+
20181982
function preconnectAs(
20191983
rel: 'preconnect' | 'dns-prefetch',
20201984
crossOrigin: null | '' | 'use-credentials',
20211985
href: string,
20221986
) {
2023-
const ownerDocument = getDocumentForPreloads();
2024-
if (typeof href === 'string' && href && ownerDocument) {
1987+
const ownerDocument = getDocumentForImperativeFloatMethods();
1988+
if (typeof href === 'string' && href) {
20251989
const limitedEscapedHref =
20261990
escapeSelectorAttributeValueInsideDoubleQuotes(href);
20271991
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
@@ -2043,6 +2007,9 @@ function preconnectAs(
20432007
}
20442008
20452009
function prefetchDNS(href: string, options?: mixed) {
2010+
if (!enableFloat) {
2011+
return;
2012+
}
20462013
if (__DEV__) {
20472014
if (typeof href !== 'string' || !href) {
20482015
console.error(
@@ -2105,10 +2072,13 @@ type PreloadOptions = {
21052072
type?: string,
21062073
};
21072074
function preload(href: string, options: PreloadOptions) {
2075+
if (!enableFloat) {
2076+
return;
2077+
}
21082078
if (__DEV__) {
21092079
validatePreloadArguments(href, options);
21102080
}
2111-
const ownerDocument = getDocumentForPreloads();
2081+
const ownerDocument = getDocumentForImperativeFloatMethods();
21122082
if (
21132083
typeof href === 'string' &&
21142084
href &&
@@ -2166,61 +2136,25 @@ type PreinitOptions = {
21662136
integrity?: string,
21672137
};
21682138
function preinit(href: string, options: PreinitOptions) {
2139+
if (!enableFloat) {
2140+
return;
2141+
}
21692142
if (__DEV__) {
21702143
validatePreinitArguments(href, options);
21712144
}
2145+
const ownerDocument = getDocumentForImperativeFloatMethods();
21722146
21732147
if (
21742148
typeof href === 'string' &&
21752149
href &&
21762150
typeof options === 'object' &&
21772151
options !== null
21782152
) {
2179-
const resourceRoot = getCurrentResourceRoot();
21802153
const as = options.as;
2181-
if (!resourceRoot) {
2182-
if (as === 'style' || as === 'script') {
2183-
// We are going to emit a preload as a best effort fallback since this preinit
2184-
// was called outside of a render. Given the passive nature of this fallback
2185-
// we do not warn in dev when props disagree if there happens to already be a
2186-
// matching preload with this href
2187-
const preloadDocument = getDocumentForPreloads();
2188-
if (preloadDocument) {
2189-
const limitedEscapedHref =
2190-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
2191-
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2192-
let key = preloadKey;
2193-
switch (as) {
2194-
case 'style':
2195-
key = getStyleKey(href);
2196-
break;
2197-
case 'script':
2198-
key = getScriptKey(href);
2199-
break;
2200-
}
2201-
if (!preloadPropsMap.has(key)) {
2202-
const preloadProps = preloadPropsFromPreinitOptions(
2203-
href,
2204-
as,
2205-
options,
2206-
);
2207-
preloadPropsMap.set(key, preloadProps);
2208-
2209-
if (null === preloadDocument.querySelector(preloadKey)) {
2210-
const instance = preloadDocument.createElement('link');
2211-
setInitialProperties(instance, 'link', preloadProps);
2212-
markNodeAsHoistable(instance);
2213-
(preloadDocument.head: any).appendChild(instance);
2214-
}
2215-
}
2216-
}
2217-
}
2218-
return;
2219-
}
22202154
22212155
switch (as) {
22222156
case 'style': {
2223-
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2157+
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
22242158
22252159
const key = getStyleKey(href);
22262160
const precedence = options.precedence || 'default';
@@ -2239,7 +2173,7 @@ function preinit(href: string, options: PreinitOptions) {
22392173
};
22402174
22412175
// Attempt to hydrate instance from DOM
2242-
let instance: null | Instance = resourceRoot.querySelector(
2176+
let instance: null | Instance = ownerDocument.querySelector(
22432177
getStylesheetSelectorFromKey(key),
22442178
);
22452179
if (instance) {
@@ -2255,7 +2189,6 @@ function preinit(href: string, options: PreinitOptions) {
22552189
if (preloadProps) {
22562190
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
22572191
}
2258-
const ownerDocument = getDocumentFromRoot(resourceRoot);
22592192
const link = (instance = ownerDocument.createElement('link'));
22602193
markNodeAsHoistable(link);
22612194
setInitialProperties(link, 'link', stylesheetProps);
@@ -2272,7 +2205,7 @@ function preinit(href: string, options: PreinitOptions) {
22722205
});
22732206
22742207
state.loading |= Inserted;
2275-
insertStylesheet(instance, precedence, resourceRoot);
2208+
insertStylesheet(instance, precedence, ownerDocument);
22762209
}
22772210
22782211
// Construct a Resource and cache it
@@ -2287,7 +2220,7 @@ function preinit(href: string, options: PreinitOptions) {
22872220
}
22882221
case 'script': {
22892222
const src = href;
2290-
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2223+
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
22912224
22922225
const key = getScriptKey(src);
22932226
@@ -2300,7 +2233,7 @@ function preinit(href: string, options: PreinitOptions) {
23002233
}
23012234
23022235
// Attempt to hydrate instance from DOM
2303-
let instance: null | Instance = resourceRoot.querySelector(
2236+
let instance: null | Instance = ownerDocument.querySelector(
23042237
getScriptSelectorFromKey(key),
23052238
);
23062239
if (!instance) {
@@ -2311,7 +2244,6 @@ function preinit(href: string, options: PreinitOptions) {
23112244
if (preloadProps) {
23122245
adoptPreloadPropsForScript(scriptProps, preloadProps);
23132246
}
2314-
const ownerDocument = getDocumentFromRoot(resourceRoot);
23152247
instance = ownerDocument.createElement('script');
23162248
markNodeAsHoistable(instance);
23172249
setInitialProperties(instance, 'link', scriptProps);
@@ -2332,20 +2264,6 @@ function preinit(href: string, options: PreinitOptions) {
23322264
}
23332265
}
23342266
2335-
function preloadPropsFromPreinitOptions(
2336-
href: string,
2337-
as: ResourceType,
2338-
options: PreinitOptions,
2339-
): PreloadProps {
2340-
return {
2341-
href,
2342-
rel: 'preload',
2343-
as,
2344-
crossOrigin: as === 'font' ? '' : options.crossOrigin,
2345-
integrity: options.integrity,
2346-
};
2347-
}
2348-
23492267
function stylesheetPropsFromPreinitOptions(
23502268
href: string,
23512269
precedence: string,

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3833,7 +3833,7 @@ body {
38333833
});
38343834

38353835
// @gate enableFloat
3836-
it('creates a preload resource when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
3836+
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
38373837
function App() {
38383838
React.useEffect(() => {
38393839
ReactDOM.preinit('foo', {as: 'style'});
@@ -3851,11 +3851,55 @@ body {
38513851
expect(getMeaningfulChildren(document)).toEqual(
38523852
<html>
38533853
<head>
3854-
<link rel="preload" href="foo" as="style" />
3854+
<link rel="stylesheet" href="foo" data-precedence="default" />
3855+
</head>
3856+
<body>foo</body>
3857+
</html>,
3858+
);
3859+
});
3860+
3861+
// @gate enableFloat
3862+
it('creates a stylesheet resource in the ownerDocument when ReactDOM.preinit(..., {as: "style" }) is called outside of render on the client', async () => {
3863+
// 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
3864+
// you would want to avoid in a real app.
3865+
const shadow = document.body.attachShadow({mode: 'open'});
3866+
function ShadowComponent() {
3867+
ReactDOM.preinit('bar', {as: 'style'});
3868+
return null;
3869+
}
3870+
function App() {
3871+
React.useEffect(() => {
3872+
ReactDOM.preinit('foo', {as: 'style'});
3873+
}, []);
3874+
return (
3875+
<html>
3876+
<body>
3877+
foo
3878+
{ReactDOM.createPortal(
3879+
<div>
3880+
<ShadowComponent />
3881+
shadow
3882+
</div>,
3883+
shadow,
3884+
)}
3885+
</body>
3886+
</html>
3887+
);
3888+
}
3889+
3890+
const root = ReactDOMClient.createRoot(document);
3891+
root.render(<App />);
3892+
await waitForAll([]);
3893+
expect(getMeaningfulChildren(document)).toEqual(
3894+
<html>
3895+
<head>
3896+
<link rel="stylesheet" href="bar" data-precedence="default" />
3897+
<link rel="stylesheet" href="foo" data-precedence="default" />
38553898
</head>
38563899
<body>foo</body>
38573900
</html>,
38583901
);
3902+
expect(getMeaningfulChildren(shadow)).toEqual(<div>shadow</div>);
38593903
});
38603904

38613905
// @gate enableFloat
@@ -3955,7 +3999,7 @@ body {
39553999
expect(getMeaningfulChildren(document)).toEqual(
39564000
<html>
39574001
<head>
3958-
<link rel="preload" href="foo" as="script" />
4002+
<script async="" src="foo" />
39594003
</head>
39604004
<body>foo</body>
39614005
</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

packages/react-native-renderer/src/ReactFiberConfigFabric.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -489,11 +489,3 @@ export function suspendInstance(type: Type, props: Props): void {}
489489
export function waitForCommitToBeReady(): null {
490490
return null;
491491
}
492-
493-
export function prepareRendererToRender(container: Container): void {
494-
// noop
495-
}
496-
497-
export function resetRendererAfterRender() {
498-
// noop
499-
}

packages/react-native-renderer/src/ReactFiberConfigNative.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -542,11 +542,3 @@ export function suspendInstance(type: Type, props: Props): void {}
542542
export function waitForCommitToBeReady(): null {
543543
return null;
544544
}
545-
546-
export function prepareRendererToRender(container: Container): void {
547-
// noop
548-
}
549-
550-
export function resetRendererAfterRender(): void {
551-
// noop
552-
}

0 commit comments

Comments
 (0)