Skip to content

Commit 2872a26

Browse files
authored
track resources in different roots separately (#25388)
* track resources in different roots separately * flow types * add test demonstrating portals deep into shadowRoots * revert hostcontext changes * lints * funge style cache key a la ReactDOMComponentTree * hide hacks in componentTree
1 parent ea04a48 commit 2872a26

File tree

8 files changed

+220
-43
lines changed

8 files changed

+220
-43
lines changed

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

+10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow
88
*/
99

10+
import type {FloatRoot, StyleResource} from './ReactDOMFloatClient';
1011
import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
1112
import type {ReactScopeInstance} from 'shared/ReactTypes';
1213
import type {
@@ -42,6 +43,7 @@ const internalContainerInstanceKey = '__reactContainer$' + randomKey;
4243
const internalEventHandlersKey = '__reactEvents$' + randomKey;
4344
const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
4445
const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
46+
const internalRootNodeStylesSetKey = '__reactStyles$' + randomKey;
4547

4648
export function detachDeletedInstance(node: Instance): void {
4749
// TODO: This function is only called on host components. I don't think all of
@@ -266,3 +268,11 @@ export function doesTargetHaveEventHandle(
266268
}
267269
return eventHandles.has(eventHandle);
268270
}
271+
272+
export function getStylesFromRoot(root: FloatRoot): Map<string, StyleResource> {
273+
let styles = (root: any)[internalRootNodeStylesSetKey];
274+
if (!styles) {
275+
styles = (root: any)[internalRootNodeStylesSetKey] = new Map();
276+
}
277+
return styles;
278+
}

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

+59-40
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@
77
* @flow
88
*/
99

10-
import type {Instance} from './ReactDOMHostConfig';
10+
import type {Instance, Container} from './ReactDOMHostConfig';
11+
1112
import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
1213
const {Dispatcher} = ReactDOMSharedInternals;
14+
import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
1315
import {
1416
validateUnmatchedLinkResourceProps,
1517
validatePreloadResourceDifference,
@@ -21,7 +23,9 @@ import {
2123
validatePreinitArguments,
2224
} from '../shared/ReactDOMResourceValidation';
2325
import {createElement, setInitialProperties} from './ReactDOMComponent';
26+
import {getStylesFromRoot} from './ReactDOMComponentTree';
2427
import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
28+
import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
2529

2630
// The resource types we support. currently they match the form for the as argument.
2731
// In the future this may need to change, especially when modules / scripts are supported
@@ -47,7 +51,7 @@ type StyleProps = {
4751
'data-rprec': string,
4852
[string]: mixed,
4953
};
50-
type StyleResource = {
54+
export type StyleResource = {
5155
type: 'style',
5256

5357
// Ref count for resource
@@ -66,7 +70,7 @@ type StyleResource = {
6670
loaded: boolean,
6771
error: mixed,
6872
instance: ?Element,
69-
ownerDocument: Document,
73+
root: FloatRoot,
7074
};
7175

7276
type Props = {[string]: mixed};
@@ -79,11 +83,6 @@ type Resource = StyleResource | PreloadResource;
7983
// e = errored
8084
type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
8185

82-
// When rendering we set the currentDocument if one exists. we use this for Resources
83-
// we encounter during render. If this is null and we are dispatching preloads and
84-
// other calls on the ReactDOM module we look for the window global and get the document from there
85-
let currentDocument: ?Document = null;
86-
8786
// It is valid to preload even when we aren't actively rendering. For cases where Float functions are
8887
// called when there is no rendering we track the last used document. It is not safe to insert
8988
// arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
@@ -93,14 +92,17 @@ let currentDocument: ?Document = null;
9392
let lastCurrentDocument: ?Document = null;
9493

9594
let previousDispatcher = null;
96-
export function prepareToRenderResources(ownerDocument: Document) {
97-
currentDocument = lastCurrentDocument = ownerDocument;
95+
export function prepareToRenderResources(rootContainer: Container) {
96+
// Flot thinks that getRootNode returns a Node but it actually returns a
97+
// Document or ShadowRoot
98+
const rootNode: FloatRoot = (rootContainer.getRootNode(): any);
99+
lastCurrentDocument = getDocumentFromRoot(rootNode);
100+
98101
previousDispatcher = Dispatcher.current;
99102
Dispatcher.current = ReactDOMClientDispatcher;
100103
}
101104

102105
export function cleanupAfterRenderResources() {
103-
currentDocument = null;
104106
Dispatcher.current = previousDispatcher;
105107
previousDispatcher = null;
106108
}
@@ -110,9 +112,16 @@ export function cleanupAfterRenderResources() {
110112
// from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
111113
export const ReactDOMClientDispatcher = {preload, preinit};
112114

115+
export type FloatRoot = Document | ShadowRoot;
116+
113117
// global maps of Resources
114118
const preloadResources: Map<string, PreloadResource> = new Map();
115-
const styleResources: Map<string, StyleResource> = new Map();
119+
120+
function getCurrentResourceRoot(): null | FloatRoot {
121+
const currentContainer = getCurrentRootHostContainer();
122+
// $FlowFixMe flow should know currentContainer is a Node and has getRootNode
123+
return currentContainer ? currentContainer.getRootNode() : null;
124+
}
116125

117126
// Preloads are somewhat special. Even if we don't have the Document
118127
// used by the root that is rendering a component trying to insert a preload
@@ -121,13 +130,22 @@ const styleResources: Map<string, StyleResource> = new Map();
121130
// lastCurrentDocument if that exists. As a fallback we will use the window.document
122131
// if available.
123132
function getDocumentForPreloads(): ?Document {
124-
try {
125-
return currentDocument || lastCurrentDocument || window.document;
126-
} catch (error) {
127-
return null;
133+
const root = getCurrentResourceRoot();
134+
if (root) {
135+
return root.ownerDocument || root;
136+
} else {
137+
try {
138+
return lastCurrentDocument || window.document;
139+
} catch (error) {
140+
return null;
141+
}
128142
}
129143
}
130144

145+
function getDocumentFromRoot(root: FloatRoot): Document {
146+
return root.ownerDocument || root;
147+
}
148+
131149
// --------------------------------------
132150
// ReactDOM.Preload
133151
// --------------------------------------
@@ -200,8 +218,9 @@ function preinit(href: string, options: PreinitOptions) {
200218
typeof options === 'object' &&
201219
options !== null
202220
) {
221+
const resourceRoot = getCurrentResourceRoot();
203222
const as = options.as;
204-
if (!currentDocument) {
223+
if (!resourceRoot) {
205224
// We are going to emit a preload as a best effort fallback since this preinit
206225
// was called outside of a render. Given the passive nature of this fallback
207226
// we do not warn in dev when props disagree if there happens to already be a
@@ -223,6 +242,7 @@ function preinit(href: string, options: PreinitOptions) {
223242

224243
switch (as) {
225244
case 'style': {
245+
const styleResources = getStylesFromRoot(resourceRoot);
226246
const precedence = options.precedence || 'default';
227247
let resource = styleResources.get(href);
228248
if (resource) {
@@ -241,8 +261,8 @@ function preinit(href: string, options: PreinitOptions) {
241261
options,
242262
);
243263
resource = createStyleResource(
244-
// $FlowFixMe[incompatible-call] found when upgrading Flow
245-
currentDocument,
264+
styleResources,
265+
resourceRoot,
246266
href,
247267
precedence,
248268
resourceProps,
@@ -303,16 +323,18 @@ export function getResource(
303323
pendingProps: Props,
304324
currentProps: null | Props,
305325
): null | Resource {
306-
if (!currentDocument) {
326+
const resourceRoot = getCurrentResourceRoot();
327+
if (!resourceRoot) {
307328
throw new Error(
308-
'"currentDocument" was expected to exist. This is a bug in React.',
329+
'"resourceRoot" was expected to exist. This is a bug in React.',
309330
);
310331
}
311332
switch (type) {
312333
case 'link': {
313334
const {rel} = pendingProps;
314335
switch (rel) {
315336
case 'stylesheet': {
337+
const styleResources = getStylesFromRoot(resourceRoot);
316338
let didWarn;
317339
if (__DEV__) {
318340
if (currentProps) {
@@ -348,8 +370,8 @@ export function getResource(
348370
} else {
349371
const resourceProps = stylePropsFromRawProps(styleRawProps);
350372
resource = createStyleResource(
351-
// $FlowFixMe[incompatible-call] found when upgrading Flow
352-
currentDocument,
373+
styleResources,
374+
resourceRoot,
353375
href,
354376
precedence,
355377
resourceProps,
@@ -384,8 +406,7 @@ export function getResource(
384406
} else {
385407
const resourceProps = preloadPropsFromRawProps(preloadRawProps);
386408
resource = createPreloadResource(
387-
// $FlowFixMe[incompatible-call] found when upgrading Flow
388-
currentDocument,
409+
getDocumentFromRoot(resourceRoot),
389410
href,
390411
resourceProps,
391412
);
@@ -463,7 +484,8 @@ function createResourceInstance(
463484
}
464485

465486
function createStyleResource(
466-
ownerDocument: Document,
487+
styleResources: Map<string, StyleResource>,
488+
root: FloatRoot,
467489
href: string,
468490
precedence: string,
469491
props: StyleProps,
@@ -479,7 +501,7 @@ function createStyleResource(
479501
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
480502
href,
481503
);
482-
const existingEl = ownerDocument.querySelector(
504+
const existingEl = root.querySelector(
483505
`link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
484506
);
485507
const resource = {
@@ -492,7 +514,7 @@ function createStyleResource(
492514
preloaded: false,
493515
loaded: false,
494516
error: false,
495-
ownerDocument,
517+
root,
496518
instance: null,
497519
};
498520
styleResources.set(href, resource);
@@ -567,7 +589,7 @@ function immediatelyPreloadStyleResource(resource: StyleResource) {
567589
const {href, props} = resource;
568590
const preloadProps = preloadPropsFromStyleProps(props);
569591
resource.hint = createPreloadResource(
570-
resource.ownerDocument,
592+
getDocumentFromRoot(resource.root),
571593
href,
572594
preloadProps,
573595
);
@@ -613,11 +635,11 @@ function createPreloadResource(
613635

614636
function acquireStyleResource(resource: StyleResource): Instance {
615637
if (!resource.instance) {
616-
const {props, ownerDocument, precedence} = resource;
638+
const {props, root, precedence} = resource;
617639
const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
618640
props.href,
619641
);
620-
const existingEl = ownerDocument.querySelector(
642+
const existingEl = root.querySelector(
621643
`link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
622644
);
623645
if (existingEl) {
@@ -649,11 +671,11 @@ function acquireStyleResource(resource: StyleResource): Instance {
649671
const instance = createResourceInstance(
650672
'link',
651673
resource.props,
652-
ownerDocument,
674+
getDocumentFromRoot(root),
653675
);
654676

655677
attachLoadListeners(instance, resource);
656-
insertStyleInstance(instance, precedence, ownerDocument);
678+
insertStyleInstance(instance, precedence, root);
657679
resource.instance = instance;
658680
}
659681
}
@@ -724,11 +746,9 @@ function onResourceError(
724746
function insertStyleInstance(
725747
instance: Instance,
726748
precedence: string,
727-
ownerDocument: Document,
749+
root: FloatRoot,
728750
): void {
729-
const nodes = ownerDocument.querySelectorAll(
730-
'link[rel="stylesheet"][data-rprec]',
731-
);
751+
const nodes = root.querySelectorAll('link[rel="stylesheet"][data-rprec]');
732752
const last = nodes.length ? nodes[nodes.length - 1] : null;
733753
let prior = last;
734754
for (let i = 0; i < nodes.length; i++) {
@@ -746,9 +766,8 @@ function insertStyleInstance(
746766
// must exist.
747767
((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
748768
} else {
749-
// @TODO call getRootNode on root.container. if it is a Document, insert into head
750-
// if it is a ShadowRoot insert it into the root node.
751-
const parent = ownerDocument.head;
769+
const parent =
770+
root.nodeType === DOCUMENT_NODE ? ((root: any): Document).head : root;
752771
if (parent) {
753772
parent.insertBefore(instance, parent.firstChild);
754773
} else {

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ import {
4141
warnForDeletedHydratableText,
4242
warnForInsertedHydratedElement,
4343
warnForInsertedHydratedText,
44-
getOwnerDocumentFromRootContainer,
4544
} from './ReactDOMComponent';
4645
import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
4746
import setTextContent from './setTextContent';
@@ -1376,7 +1375,7 @@ function isHostResourceInstance(instance: Instance | Container): boolean {
13761375

13771376
export function prepareRendererToRender(rootContainer: Container) {
13781377
if (enableFloat) {
1379-
prepareToRenderResources(getOwnerDocumentFromRootContainer(rootContainer));
1378+
prepareToRenderResources(rootContainer);
13801379
}
13811380
}
13821381

0 commit comments

Comments
 (0)