@samitouri / QOS-React-2 / commits / 2872a26e14

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

Josh Story committed Oct 4, 2022 at 16:11 UTC 2872a26e14627f90e812048f21feb6d0c8cc0244
8 files changed +220 -43
packages/react-dom-bindings/src/client/ReactDOMComponentTree.js
+10
@@ -7,6 +7,7 @@
7 * @flow
8 */
9
10 +import type {FloatRoot, StyleResource} from './ReactDOMFloatClient';
11 import type {Fiber} from 'react-reconciler/src/ReactInternalTypes';
12 import type {ReactScopeInstance} from 'shared/ReactTypes';
13 import type {
@@ -42,6 +43,7 @@ const internalContainerInstanceKey = '__reactContainer$' + randomKey;
43 const internalEventHandlersKey = '__reactEvents$' + randomKey;
44 const internalEventHandlerListenersKey = '__reactListeners$' + randomKey;
45 const internalEventHandlesSetKey = '__reactHandles$' + randomKey;
46 +const internalRootNodeStylesSetKey = '__reactStyles$' + randomKey;
47
48 export function detachDeletedInstance(node: Instance): void {
49 // TODO: This function is only called on host components. I don't think all of
@@ -266,3 +268,11 @@ export function doesTargetHaveEventHandle(
268 }
269 return eventHandles.has(eventHandle);
270 }
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
@@ -7,9 +7,11 @@
7 * @flow
8 */
9
10 -import type {Instance} from './ReactDOMHostConfig';
10 +import type {Instance, Container} from './ReactDOMHostConfig';
11 +
12 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
13 const {Dispatcher} = ReactDOMSharedInternals;
14 +import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
15 import {
16 validateUnmatchedLinkResourceProps,
17 validatePreloadResourceDifference,
@@ -21,7 +23,9 @@ import {
23 validatePreinitArguments,
24 } from '../shared/ReactDOMResourceValidation';
25 import {createElement, setInitialProperties} from './ReactDOMComponent';
26 +import {getStylesFromRoot} from './ReactDOMComponentTree';
27 import {HTML_NAMESPACE} from '../shared/DOMNamespaces';
28 +import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
29
30 // The resource types we support. currently they match the form for the as argument.
31 // In the future this may need to change, especially when modules / scripts are supported
@@ -47,7 +51,7 @@ type StyleProps = {
51 'data-rprec': string,
52 [string]: mixed,
53 };
50 -type StyleResource = {
54 +export type StyleResource = {
55 type: 'style',
56
57 // Ref count for resource
@@ -66,7 +70,7 @@ type StyleResource = {
70 loaded: boolean,
71 error: mixed,
72 instance: ?Element,
69 - ownerDocument: Document,
73 + root: FloatRoot,
74 };
75
76 type Props = {[string]: mixed};
@@ -79,11 +83,6 @@ type Resource = StyleResource | PreloadResource;
83 // e = errored
84 type StyleResourceLoadingState = Promise<mixed> & {s?: 'l' | 'e'};
85
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 -
86 // It is valid to preload even when we aren't actively rendering. For cases where Float functions are
87 // called when there is no rendering we track the last used document. It is not safe to insert
88 // arbitrary resources into the lastCurrentDocument b/c it may not actually be the document
@@ -93,14 +92,17 @@ let currentDocument: ?Document = null;
92 let lastCurrentDocument: ?Document = null;
93
94 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 +
101 previousDispatcher = Dispatcher.current;
102 Dispatcher.current = ReactDOMClientDispatcher;
103 }
104
105 export function cleanupAfterRenderResources() {
103 - currentDocument = null;
106 Dispatcher.current = previousDispatcher;
107 previousDispatcher = null;
108 }
@@ -110,9 +112,16 @@ export function cleanupAfterRenderResources() {
112 // from Internals -> ReactDOM -> FloatClient -> Internals so this doesn't introduce a new one.
113 export const ReactDOMClientDispatcher = {preload, preinit};
114
115 +export type FloatRoot = Document | ShadowRoot;
116 +
117 // global maps of Resources
118 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 +}
125
126 // Preloads are somewhat special. Even if we don't have the Document
127 // 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();
130 // lastCurrentDocument if that exists. As a fallback we will use the window.document
131 // if available.
132 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 + }
142 }
143 }
144
145 +function getDocumentFromRoot(root: FloatRoot): Document {
146 + return root.ownerDocument || root;
147 +}
148 +
149 // --------------------------------------
150 // ReactDOM.Preload
151 // --------------------------------------
@@ -200,8 +218,9 @@ function preinit(href: string, options: PreinitOptions) {
218 typeof options === 'object' &&
219 options !== null
220 ) {
221 + const resourceRoot = getCurrentResourceRoot();
222 const as = options.as;
204 - if (!currentDocument) {
223 + if (!resourceRoot) {
224 // We are going to emit a preload as a best effort fallback since this preinit
225 // was called outside of a render. Given the passive nature of this fallback
226 // 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) {
242
243 switch (as) {
244 case 'style': {
245 + const styleResources = getStylesFromRoot(resourceRoot);
246 const precedence = options.precedence || 'default';
247 let resource = styleResources.get(href);
248 if (resource) {
@@ -241,8 +261,8 @@ function preinit(href: string, options: PreinitOptions) {
261 options,
262 );
263 resource = createStyleResource(
244 - // $FlowFixMe[incompatible-call] found when upgrading Flow
245 - currentDocument,
264 + styleResources,
265 + resourceRoot,
266 href,
267 precedence,
268 resourceProps,
@@ -303,9 +323,10 @@ export function getResource(
323 pendingProps: Props,
324 currentProps: null | Props,
325 ): null | Resource {
306 - if (!currentDocument) {
326 + const resourceRoot = getCurrentResourceRoot();
327 + if (!resourceRoot) {
328 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.',
330 );
331 }
332 switch (type) {
@@ -313,6 +334,7 @@ export function getResource(
334 const {rel} = pendingProps;
335 switch (rel) {
336 case 'stylesheet': {
337 + const styleResources = getStylesFromRoot(resourceRoot);
338 let didWarn;
339 if (__DEV__) {
340 if (currentProps) {
@@ -348,8 +370,8 @@ export function getResource(
370 } else {
371 const resourceProps = stylePropsFromRawProps(styleRawProps);
372 resource = createStyleResource(
351 - // $FlowFixMe[incompatible-call] found when upgrading Flow
352 - currentDocument,
373 + styleResources,
374 + resourceRoot,
375 href,
376 precedence,
377 resourceProps,
@@ -384,8 +406,7 @@ export function getResource(
406 } else {
407 const resourceProps = preloadPropsFromRawProps(preloadRawProps);
408 resource = createPreloadResource(
387 - // $FlowFixMe[incompatible-call] found when upgrading Flow
388 - currentDocument,
409 + getDocumentFromRoot(resourceRoot),
410 href,
411 resourceProps,
412 );
@@ -463,7 +484,8 @@ function createResourceInstance(
484 }
485
486 function createStyleResource(
466 - ownerDocument: Document,
487 + styleResources: Map<string, StyleResource>,
488 + root: FloatRoot,
489 href: string,
490 precedence: string,
491 props: StyleProps,
@@ -479,7 +501,7 @@ function createStyleResource(
501 const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
502 href,
503 );
482 - const existingEl = ownerDocument.querySelector(
504 + const existingEl = root.querySelector(
505 `link[rel="stylesheet"][href="${limitedEscapedHref}"]`,
506 );
507 const resource = {
@@ -492,7 +514,7 @@ function createStyleResource(
514 preloaded: false,
515 loaded: false,
516 error: false,
495 - ownerDocument,
517 + root,
518 instance: null,
519 };
520 styleResources.set(href, resource);
@@ -567,7 +589,7 @@ function immediatelyPreloadStyleResource(resource: StyleResource) {
589 const {href, props} = resource;
590 const preloadProps = preloadPropsFromStyleProps(props);
591 resource.hint = createPreloadResource(
570 - resource.ownerDocument,
592 + getDocumentFromRoot(resource.root),
593 href,
594 preloadProps,
595 );
@@ -613,11 +635,11 @@ function createPreloadResource(
635
636 function acquireStyleResource(resource: StyleResource): Instance {
637 if (!resource.instance) {
616 - const {props, ownerDocument, precedence} = resource;
638 + const {props, root, precedence} = resource;
639 const limitedEscapedHref = escapeSelectorAttributeValueInsideDoubleQuotes(
640 props.href,
641 );
620 - const existingEl = ownerDocument.querySelector(
642 + const existingEl = root.querySelector(
643 `link[rel="stylesheet"][data-rprec][href="${limitedEscapedHref}"]`,
644 );
645 if (existingEl) {
@@ -649,11 +671,11 @@ function acquireStyleResource(resource: StyleResource): Instance {
671 const instance = createResourceInstance(
672 'link',
673 resource.props,
652 - ownerDocument,
674 + getDocumentFromRoot(root),
675 );
676
677 attachLoadListeners(instance, resource);
656 - insertStyleInstance(instance, precedence, ownerDocument);
678 + insertStyleInstance(instance, precedence, root);
679 resource.instance = instance;
680 }
681 }
@@ -724,11 +746,9 @@ function onResourceError(
746 function insertStyleInstance(
747 instance: Instance,
748 precedence: string,
727 - ownerDocument: Document,
749 + root: FloatRoot,
750 ): void {
729 - const nodes = ownerDocument.querySelectorAll(
730 - 'link[rel="stylesheet"][data-rprec]',
731 - );
751 + const nodes = root.querySelectorAll('link[rel="stylesheet"][data-rprec]');
752 const last = nodes.length ? nodes[nodes.length - 1] : null;
753 let prior = last;
754 for (let i = 0; i < nodes.length; i++) {
@@ -746,9 +766,8 @@ function insertStyleInstance(
766 // must exist.
767 ((prior.parentNode: any): Node).insertBefore(instance, prior.nextSibling);
768 } 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;
771 if (parent) {
772 parent.insertBefore(instance, parent.firstChild);
773 } else {
packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+1 -2
@@ -41,7 +41,6 @@ import {
41 warnForDeletedHydratableText,
42 warnForInsertedHydratedElement,
43 warnForInsertedHydratedText,
44 - getOwnerDocumentFromRootContainer,
44 } from './ReactDOMComponent';
45 import {getSelectionInformation, restoreSelection} from './ReactInputSelection';
46 import setTextContent from './setTextContent';
@@ -1376,7 +1375,7 @@ function isHostResourceInstance(instance: Instance | Container): boolean {
1375
1376 export function prepareRendererToRender(rootContainer: Container) {
1377 if (enableFloat) {
1379 - prepareToRenderResources(getOwnerDocumentFromRootContainer(rootContainer));
1378 + prepareToRenderResources(rootContainer);
1379 }
1380 }
1381
packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+116
@@ -552,6 +552,122 @@ describe('ReactDOMFloat', () => {
552 });
553 });
554
555 + describe('document encapsulation', () => {
556 + // @gate enableFloat
557 + it('can support styles inside portals to a shadowRoot', async () => {
558 + const shadow = document.body.attachShadow({mode: 'open'});
559 + const root = ReactDOMClient.createRoot(container);
560 + root.render(
561 + <>
562 + <link rel="stylesheet" href="foo" precedence="default" />
563 + {ReactDOM.createPortal(
564 + <div>
565 + <link
566 + rel="stylesheet"
567 + href="foo"
568 + data-extra-prop="foo"
569 + precedence="different"
570 + />
571 + shadow
572 + </div>,
573 + shadow,
574 + )}
575 + container
576 + </>,
577 + );
578 + expect(Scheduler).toFlushWithoutYielding();
579 + expect(getVisibleChildren(document)).toEqual(
580 + <html>
581 + <head>
582 + <link rel="stylesheet" href="foo" data-rprec="default" />
583 + <link rel="preload" href="foo" as="style" />
584 + </head>
585 + <body>
586 + <div id="container">container</div>
587 + </body>
588 + </html>,
589 + );
590 + expect(getVisibleChildren(shadow)).toEqual([
591 + <link
592 + rel="stylesheet"
593 + href="foo"
594 + data-rprec="different"
595 + data-extra-prop="foo"
596 + />,
597 + <div>shadow</div>,
598 + ]);
599 + });
600 + // @gate enableFloat
601 + it('can support styles inside portals to an element in shadowRoots', async () => {
602 + const template = document.createElement('template');
603 + template.innerHTML =
604 + "<div><div id='shadowcontainer1'></div><div id='shadowcontainer2'></div></div>";
605 + const shadow = document.body.attachShadow({mode: 'open'});
606 + shadow.appendChild(template.content);
607 +
608 + const shadowContainer1 = shadow.getElementById('shadowcontainer1');
609 + const shadowContainer2 = shadow.getElementById('shadowcontainer2');
610 + const root = ReactDOMClient.createRoot(container);
611 + root.render(
612 + <>
613 + <link rel="stylesheet" href="foo" precedence="default" />
614 + {ReactDOM.createPortal(
615 + <div>
616 + <link rel="stylesheet" href="foo" precedence="one" />
617 + <link rel="stylesheet" href="bar" precedence="two" />1
618 + </div>,
619 + shadow,
620 + )}
621 + {ReactDOM.createPortal(
622 + <div>
623 + <link rel="stylesheet" href="foo" precedence="one" />
624 + <link rel="stylesheet" href="baz" precedence="one" />2
625 + </div>,
626 + shadowContainer1,
627 + )}
628 + {ReactDOM.createPortal(
629 + <div>
630 + <link rel="stylesheet" href="bar" precedence="two" />
631 + <link rel="stylesheet" href="qux" precedence="three" />3
632 + </div>,
633 + shadowContainer2,
634 + )}
635 + container
636 + </>,
637 + );
638 + expect(Scheduler).toFlushWithoutYielding();
639 + expect(getVisibleChildren(document)).toEqual(
640 + <html>
641 + <head>
642 + <link rel="stylesheet" href="foo" data-rprec="default" />
643 + <link rel="preload" href="foo" as="style" />
644 + <link rel="preload" href="bar" as="style" />
645 + <link rel="preload" href="baz" as="style" />
646 + <link rel="preload" href="qux" as="style" />
647 + </head>
648 + <body>
649 + <div id="container">container</div>
650 + </body>
651 + </html>,
652 + );
653 + expect(getVisibleChildren(shadow)).toEqual([
654 + <link rel="stylesheet" href="foo" data-rprec="one" />,
655 + <link rel="stylesheet" href="baz" data-rprec="one" />,
656 + <link rel="stylesheet" href="bar" data-rprec="two" />,
657 + <link rel="stylesheet" href="qux" data-rprec="three" />,
658 + <div>
659 + <div id="shadowcontainer1">
660 + <div>2</div>
661 + </div>
662 + <div id="shadowcontainer2">
663 + <div>3</div>
664 + </div>
665 + </div>,
666 + <div>1</div>,
667 + ]);
668 + });
669 + });
670 +
671 describe('style resources', () => {
672 // @gate enableFloat
673 it('treats link rel stylesheet elements as a style resource when it includes a precedence when server rendering', async () => {
packages/react-reconciler/src/ReactFiberHostContext.js new
+21
@@ -0,0 +1,21 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {Container} from './ReactFiberHostConfig';
11 +import {enableNewReconciler} from 'shared/ReactFeatureFlags';
12 +
13 +import {getCurrentRootHostContainer as getCurrentRootHostContainer_old} from './ReactFiberHostContext.old';
14 +
15 +import {getCurrentRootHostContainer as getCurrentRootHostContainer_new} from './ReactFiberHostContext.new';
16 +
17 +export function getCurrentRootHostContainer(): null | Container {
18 + return enableNewReconciler
19 + ? getCurrentRootHostContainer_new()
20 + : getCurrentRootHostContainer_old();
21 +}
packages/react-reconciler/src/ReactFiberHostContext.new.js
+6
@@ -38,6 +38,11 @@ function requiredContext<Value>(c: Value | NoContextT): Value {
38 return (c: any);
39 }
40
41 +function getCurrentRootHostContainer(): null | Container {
42 + const container = rootInstanceStackCursor.current;
43 + return container === NO_CONTEXT ? null : (container: any);
44 +}
45 +
46 function getRootHostContainer(): Container {
47 const rootInstance = requiredContext(rootInstanceStackCursor.current);
48 return rootInstance;
@@ -101,6 +106,7 @@ function popHostContext(fiber: Fiber): void {
106 }
107
108 export {
109 + getCurrentRootHostContainer,
110 getHostContext,
111 getRootHostContainer,
112 popHostContainer,
packages/react-reconciler/src/ReactFiberHostContext.old.js
+6
@@ -38,6 +38,11 @@ function requiredContext<Value>(c: Value | NoContextT): Value {
38 return (c: any);
39 }
40
41 +function getCurrentRootHostContainer(): null | Container {
42 + const container = rootInstanceStackCursor.current;
43 + return container === NO_CONTEXT ? null : (container: any);
44 +}
45 +
46 function getRootHostContainer(): Container {
47 const rootInstance = requiredContext(rootInstanceStackCursor.current);
48 return rootInstance;
@@ -101,6 +106,7 @@ function popHostContext(fiber: Fiber): void {
106 }
107
108 export {
109 + getCurrentRootHostContainer,
110 getHostContext,
111 getRootHostContainer,
112 popHostContainer,
scripts/error-codes/codes.json
+1 -1
@@ -431,6 +431,6 @@
431 "443": "acquireResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
432 "444": "getResource encountered a resource type it did not expect: \"%s\". this is a bug in React.",
433 "445": "\"currentResources\" was expected to exist. This is a bug in React.",
434 - "446": "\"currentDocument\" was expected to exist. This is a bug in React.",
434 + "446": "\"resourceRoot\" was expected to exist. This is a bug in React.",
435 "447": "While attempting to insert a Resource, React expected the Document to contain a head element but it was not found."
436 }