Skip to content

Commit 0bdcfff

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. When Float was first implemented the HostDispatcher was set and unset during each render. Now that we support dispatching globally the host extensions that did this Dispatcher shuffling is now a noop. Since these methods exist in all HostConfigs and they noop everywhere now we shoudl just remove them.
1 parent 9f49797 commit 0bdcfff

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
@@ -475,11 +475,3 @@ export function suspendInstance(type, props) {}
475475
export function waitForCommitToBeReady() {
476476
return null;
477477
}
478-
// eslint-disable-next-line no-undef
479-
export function prepareRendererToRender(container: Container): void {
480-
// noop
481-
}
482-
483-
export function resetRendererAfterRender(): void {
484-
// noop
485-
}

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,
@@ -1932,31 +1930,6 @@ export function prepareToCommitHoistables() {
19321930
tagCaches = null;
19331931
}
19341932

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

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

1968+
// We expect this to get inlined. It is a function mostly to communicate the special nature of
1969+
// how we resolve the HoistableRoot for ReactDOM.pre*() methods. Because we support calling
1970+
// these methods outside of render there is no way to know which Document or ShadowRoot is 'scoped'
1971+
// and so we have to fall back to something universal. Currently we just refer to the global document.
1972+
// This is notable because nowhere else in ReactDOM do we actually reference the global document or window
1973+
// because we may be rendering inside an iframe.
1974+
function getDocumentForImperativeFloatMethods(): Document {
1975+
return document;
1976+
}
1977+
20141978
function preconnectAs(
20151979
rel: 'preconnect' | 'dns-prefetch',
20161980
crossOrigin: null | '' | 'use-credentials',
20171981
href: string,
20181982
) {
2019-
const ownerDocument = getDocumentForPreloads();
2020-
if (typeof href === 'string' && href && ownerDocument) {
1983+
const ownerDocument = getDocumentForImperativeFloatMethods();
1984+
if (typeof href === 'string' && href) {
20211985
const limitedEscapedHref =
20221986
escapeSelectorAttributeValueInsideDoubleQuotes(href);
20231987
let key = `link[rel="${rel}"][href="${limitedEscapedHref}"]`;
@@ -2039,6 +2003,9 @@ function preconnectAs(
20392003
}
20402004
20412005
function prefetchDNS(href: string, options?: mixed) {
2006+
if (!enableFloat) {
2007+
return;
2008+
}
20422009
if (__DEV__) {
20432010
if (typeof href !== 'string' || !href) {
20442011
console.error(
@@ -2101,10 +2068,13 @@ type PreloadOptions = {
21012068
type?: string,
21022069
};
21032070
function preload(href: string, options: PreloadOptions) {
2071+
if (!enableFloat) {
2072+
return;
2073+
}
21042074
if (__DEV__) {
21052075
validatePreloadArguments(href, options);
21062076
}
2107-
const ownerDocument = getDocumentForPreloads();
2077+
const ownerDocument = getDocumentForImperativeFloatMethods();
21082078
if (
21092079
typeof href === 'string' &&
21102080
href &&
@@ -2162,61 +2132,25 @@ type PreinitOptions = {
21622132
integrity?: string,
21632133
};
21642134
function preinit(href: string, options: PreinitOptions) {
2135+
if (!enableFloat) {
2136+
return;
2137+
}
21652138
if (__DEV__) {
21662139
validatePreinitArguments(href, options);
21672140
}
2141+
const ownerDocument = getDocumentForImperativeFloatMethods();
21682142
21692143
if (
21702144
typeof href === 'string' &&
21712145
href &&
21722146
typeof options === 'object' &&
21732147
options !== null
21742148
) {
2175-
const resourceRoot = getCurrentResourceRoot();
21762149
const as = options.as;
2177-
if (!resourceRoot) {
2178-
if (as === 'style' || as === 'script') {
2179-
// We are going to emit a preload as a best effort fallback since this preinit
2180-
// was called outside of a render. Given the passive nature of this fallback
2181-
// we do not warn in dev when props disagree if there happens to already be a
2182-
// matching preload with this href
2183-
const preloadDocument = getDocumentForPreloads();
2184-
if (preloadDocument) {
2185-
const limitedEscapedHref =
2186-
escapeSelectorAttributeValueInsideDoubleQuotes(href);
2187-
const preloadKey = `link[rel="preload"][as="${as}"][href="${limitedEscapedHref}"]`;
2188-
let key = preloadKey;
2189-
switch (as) {
2190-
case 'style':
2191-
key = getStyleKey(href);
2192-
break;
2193-
case 'script':
2194-
key = getScriptKey(href);
2195-
break;
2196-
}
2197-
if (!preloadPropsMap.has(key)) {
2198-
const preloadProps = preloadPropsFromPreinitOptions(
2199-
href,
2200-
as,
2201-
options,
2202-
);
2203-
preloadPropsMap.set(key, preloadProps);
2204-
2205-
if (null === preloadDocument.querySelector(preloadKey)) {
2206-
const instance = preloadDocument.createElement('link');
2207-
setInitialProperties(instance, 'link', preloadProps);
2208-
markNodeAsHoistable(instance);
2209-
(preloadDocument.head: any).appendChild(instance);
2210-
}
2211-
}
2212-
}
2213-
}
2214-
return;
2215-
}
22162150
22172151
switch (as) {
22182152
case 'style': {
2219-
const styles = getResourcesFromRoot(resourceRoot).hoistableStyles;
2153+
const styles = getResourcesFromRoot(ownerDocument).hoistableStyles;
22202154
22212155
const key = getStyleKey(href);
22222156
const precedence = options.precedence || 'default';
@@ -2235,7 +2169,7 @@ function preinit(href: string, options: PreinitOptions) {
22352169
};
22362170
22372171
// Attempt to hydrate instance from DOM
2238-
let instance: null | Instance = resourceRoot.querySelector(
2172+
let instance: null | Instance = ownerDocument.querySelector(
22392173
getStylesheetSelectorFromKey(key),
22402174
);
22412175
if (instance) {
@@ -2251,7 +2185,6 @@ function preinit(href: string, options: PreinitOptions) {
22512185
if (preloadProps) {
22522186
adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
22532187
}
2254-
const ownerDocument = getDocumentFromRoot(resourceRoot);
22552188
const link = (instance = ownerDocument.createElement('link'));
22562189
markNodeAsHoistable(link);
22572190
setInitialProperties(link, 'link', stylesheetProps);
@@ -2268,7 +2201,7 @@ function preinit(href: string, options: PreinitOptions) {
22682201
});
22692202
22702203
state.loading |= Inserted;
2271-
insertStylesheet(instance, precedence, resourceRoot);
2204+
insertStylesheet(instance, precedence, ownerDocument);
22722205
}
22732206
22742207
// Construct a Resource and cache it
@@ -2283,7 +2216,7 @@ function preinit(href: string, options: PreinitOptions) {
22832216
}
22842217
case 'script': {
22852218
const src = href;
2286-
const scripts = getResourcesFromRoot(resourceRoot).hoistableScripts;
2219+
const scripts = getResourcesFromRoot(ownerDocument).hoistableScripts;
22872220
22882221
const key = getScriptKey(src);
22892222
@@ -2296,7 +2229,7 @@ function preinit(href: string, options: PreinitOptions) {
22962229
}
22972230
22982231
// Attempt to hydrate instance from DOM
2299-
let instance: null | Instance = resourceRoot.querySelector(
2232+
let instance: null | Instance = ownerDocument.querySelector(
23002233
getScriptSelectorFromKey(key),
23012234
);
23022235
if (!instance) {
@@ -2307,7 +2240,6 @@ function preinit(href: string, options: PreinitOptions) {
23072240
if (preloadProps) {
23082241
adoptPreloadPropsForScript(scriptProps, preloadProps);
23092242
}
2310-
const ownerDocument = getDocumentFromRoot(resourceRoot);
23112243
instance = ownerDocument.createElement('script');
23122244
markNodeAsHoistable(instance);
23132245
setInitialProperties(instance, 'link', scriptProps);
@@ -2328,20 +2260,6 @@ function preinit(href: string, options: PreinitOptions) {
23282260
}
23292261
}
23302262
2331-
function preloadPropsFromPreinitOptions(
2332-
href: string,
2333-
as: ResourceType,
2334-
options: PreinitOptions,
2335-
): PreloadProps {
2336-
return {
2337-
href,
2338-
rel: 'preload',
2339-
as,
2340-
crossOrigin: as === 'font' ? '' : options.crossOrigin,
2341-
integrity: options.integrity,
2342-
};
2343-
}
2344-
23452263
function stylesheetPropsFromPreinitOptions(
23462264
href: string,
23472265
precedence: string,

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

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3831,7 +3831,7 @@ body {
38313831
});
38323832

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

38593903
// @gate enableFloat
@@ -3953,7 +3997,7 @@ body {
39533997
expect(getMeaningfulChildren(document)).toEqual(
39543998
<html>
39553999
<head>
3956-
<link rel="preload" href="foo" as="script" />
4000+
<script async="" src="foo" />
39574001
</head>
39584002
<body>foo</body>
39594003
</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
@@ -485,11 +485,3 @@ export function suspendInstance(type: Type, props: Props): void {}
485485
export function waitForCommitToBeReady(): null {
486486
return null;
487487
}
488-
489-
export function prepareRendererToRender(container: Container): void {
490-
// noop
491-
}
492-
493-
export function resetRendererAfterRender() {
494-
// noop
495-
}

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

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -538,11 +538,3 @@ export function suspendInstance(type: Type, props: Props): void {}
538538
export function waitForCommitToBeReady(): null {
539539
return null;
540540
}
541-
542-
export function prepareRendererToRender(container: Container): void {
543-
// noop
544-
}
545-
546-
export function resetRendererAfterRender(): void {
547-
// noop
548-
}

0 commit comments

Comments
 (0)