Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement HostSingleton type #25010

Closed
wants to merge 2 commits into from

Conversation

gnoff
Copy link
Collaborator

@gnoff gnoff commented Jul 29, 2022

Background

For a variety of reasons html, head, and body nodes are special in the browser and historically there has not been much heed given to these elements in particular within react-dom which has led to some limitations of interoperability with externals systems such as 3rd party scripts and browser extensions. Additionally new features in React will require some special handling of these instances to be possible.

The main issue has to do with instance lifecycles and React's total ownership of the nodes within it's tree. For the head for instance, if there are stylesheet links inserted by 3rd parties, React might unmount those nodes, or reinsert them somewhere else causing a new fetch and temporary style unloading. Additionally useInsertionEffect runs in the mutation phase but if we are going to be replacing the head it will be unmounted during the time these effects are run so you can't always inject styles when you expected to be able to.

The historical advice has been to render into an element in the body that isn't like to be targeted by any external systems however this conflicts with the guidance for using streaming rendering available in React 18 where it is going to be common that React owns the entire document.

General Approach

To solve these issues, this PR introduces a new fiber type HostSingleton. Currently only react-dom has an implementation for this type and all other renderers still only use HostComponent.

  1. All HostSingletons are placed individually, they will not be appended to a parent fiber even if they are part of a tree that is going to be Placed in the same commit.
  2. During completeWork HostSingletons do not append any HostComponent children
  3. During commitWork HostSingletons if a Placement effect is needed a special Singleton placement method from the HostConfig is used. Child fibers of the fiber are then appended.
  4. During commitWork if a HostSingleton is having Placement effects appended to it, it will use optional additional semantics for finding an insertion edge. This allows for non-react-controlled children to be siblings of Placed fibers without incorrect ordering.

react-dom Approach

In the case of react-dom there are three singleton instances, document.documentElement, document.head, document.body. If you render a <html /> | <head /> | <body /> in your tree they each will bind to the already existing Element and not recreate a new one.

Key Constraints:

  • none of the 3 singleton instances will be unmounted at any time
  • none of the 3 singleton instances will be ever change referential identity
  • Head and Body nodes will never reposition, reorder, or otherwise alter the placement of style-related nodes outside of React

insertion stability (Body and Head only)

In Body and Head, we expect there to be Nodes that were created by systems other than React. Most items can safely be removed once loaded because they are fire-and-forget, such as scripts. However <style> and <link rel="stylesheet"> nodes need to be retained for proper functioning of a site and as such we expect that these two instances will have a number of Nodes outside the purview of React's runtime which change insertion semantics.

@sizebot
Copy link

sizebot commented Jul 29, 2022

Comparing: 6ef466c...ec38036

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable/react-dom/cjs/react-dom.production.min.js +0.16% 134.28 kB 134.50 kB +0.10% 42.94 kB 42.98 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +1.59% 140.35 kB 142.57 kB +1.71% 44.74 kB 45.50 kB
facebook-www/ReactDOM-prod.classic.js +0.10% 474.44 kB 474.89 kB +0.11% 84.87 kB 84.97 kB
facebook-www/ReactDOM-prod.modern.js +0.09% 459.68 kB 460.11 kB +0.10% 82.63 kB 82.71 kB
facebook-www/ReactDOMForked-prod.classic.js +0.10% 474.44 kB 474.89 kB +0.11% 84.88 kB 84.97 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-experimental/react-dom/umd/react-dom.production.min.js +1.60% 140.38 kB 142.63 kB +1.57% 45.40 kB 46.12 kB
oss-experimental/react-dom/cjs/react-dom.production.min.js +1.59% 140.35 kB 142.57 kB +1.71% 44.74 kB 45.50 kB
oss-experimental/react-dom/umd/react-dom.profiling.min.js +1.50% 149.24 kB 151.48 kB +1.41% 47.66 kB 48.34 kB
oss-experimental/react-dom/cjs/react-dom.profiling.min.js +1.48% 149.87 kB 152.09 kB +1.72% 47.08 kB 47.90 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.development.js +1.45% 17.97 kB 18.23 kB +1.37% 5.19 kB 5.26 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.development.js +1.45% 17.97 kB 18.23 kB +1.37% 5.19 kB 5.26 kB
oss-experimental/react-reconciler/cjs/react-reconciler.production.min.js +1.28% 100.87 kB 102.17 kB +0.92% 30.81 kB 31.09 kB
oss-experimental/react-reconciler/cjs/react-reconciler.profiling.min.js +1.18% 109.77 kB 111.06 kB +0.94% 32.96 kB 33.27 kB
oss-experimental/react-dom/cjs/react-dom.development.js +1.01% 1,089.28 kB 1,100.32 kB +0.83% 242.94 kB 244.95 kB
oss-experimental/react-dom/umd/react-dom.development.js +1.01% 1,142.60 kB 1,154.17 kB +0.85% 245.62 kB 247.71 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.production.min.js +0.90% 2.67 kB 2.69 kB +0.35% 1.14 kB 1.14 kB
oss-experimental/react-reconciler/cjs/react-reconciler.development.js +0.89% 805.91 kB 813.11 kB +0.58% 170.76 kB 171.75 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +0.60% 18.33 kB 18.44 kB +0.32% 5.27 kB 5.28 kB
oss-experimental/react-art/cjs/react-art.production.min.js +0.43% 89.32 kB 89.71 kB +0.29% 27.61 kB 27.69 kB
oss-experimental/react-art/umd/react-art.production.min.js +0.43% 125.16 kB 125.69 kB +0.38% 38.80 kB 38.95 kB
oss-experimental/react-art/cjs/react-art.development.js +0.42% 738.25 kB 741.38 kB +0.36% 158.48 kB 159.05 kB
oss-stable-semver/react-dom/cjs/react-dom-test-utils.development.js +0.39% 55.07 kB 55.29 kB +0.41% 15.99 kB 16.06 kB
oss-stable/react-dom/cjs/react-dom-test-utils.development.js +0.39% 55.07 kB 55.29 kB +0.41% 15.99 kB 16.06 kB
oss-experimental/react-art/umd/react-art.development.js +0.39% 844.28 kB 847.57 kB +0.33% 176.65 kB 177.24 kB
oss-stable-semver/react-dom/umd/react-dom-test-utils.development.js +0.38% 58.15 kB 58.37 kB +0.40% 16.25 kB 16.31 kB
oss-stable/react-dom/umd/react-dom-test-utils.development.js +0.38% 58.15 kB 58.37 kB +0.40% 16.25 kB 16.31 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.production.min.js +0.33% 95.45 kB 95.76 kB +0.24% 29.28 kB 29.35 kB
oss-stable/react-reconciler/cjs/react-reconciler.production.min.js +0.33% 95.47 kB 95.78 kB +0.25% 29.30 kB 29.37 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.development.js +0.33% 774.24 kB 776.76 kB +0.30% 164.35 kB 164.85 kB
oss-stable/react-reconciler/cjs/react-reconciler.development.js +0.33% 774.26 kB 776.78 kB +0.30% 164.37 kB 164.87 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler.profiling.min.js +0.30% 104.33 kB 104.64 kB +0.26% 31.51 kB 31.59 kB
oss-stable/react-reconciler/cjs/react-reconciler.profiling.min.js +0.30% 104.35 kB 104.67 kB +0.26% 31.53 kB 31.61 kB
facebook-www/ReactTestUtils-dev.modern.js +0.30% 49.53 kB 49.68 kB +0.46% 13.85 kB 13.92 kB
facebook-www/ReactTestUtils-dev.classic.js +0.30% 49.53 kB 49.68 kB +0.46% 13.85 kB 13.91 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.production.min.js +0.23% 91.49 kB 91.71 kB +0.12% 28.26 kB 28.29 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.production.min.js +0.23% 91.52 kB 91.73 kB +0.12% 28.26 kB 28.29 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.production.min.js +0.23% 91.74 kB 91.96 kB +0.11% 28.69 kB 28.72 kB
oss-stable/react-test-renderer/umd/react-test-renderer.production.min.js +0.23% 91.76 kB 91.98 kB +0.11% 28.69 kB 28.72 kB
facebook-www/ReactDOMTesting-dev.modern.js +0.23% 1,078.29 kB 1,080.72 kB +0.24% 240.40 kB 240.96 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.production.min.js +0.22% 96.24 kB 96.45 kB +0.16% 29.59 kB 29.63 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.production.min.js +0.22% 96.48 kB 96.70 kB +0.23% 29.95 kB 30.02 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.production.min.js +0.22% 145.10 kB 145.43 kB +0.15% 46.59 kB 46.66 kB
facebook-www/ReactDOMTesting-dev.classic.js +0.22% 1,106.91 kB 1,109.37 kB +0.22% 246.22 kB 246.76 kB
oss-stable-semver/react-test-renderer/umd/react-test-renderer.development.js +0.22% 712.82 kB 714.36 kB +0.24% 148.96 kB 149.31 kB
oss-stable/react-test-renderer/umd/react-test-renderer.development.js +0.22% 712.84 kB 714.39 kB +0.24% 148.98 kB 149.34 kB
oss-experimental/react-dom/cjs/react-dom-unstable_testing.development.js +0.21% 1,079.32 kB 1,081.61 kB +0.23% 240.90 kB 241.45 kB
oss-stable-semver/react-test-renderer/cjs/react-test-renderer.development.js +0.21% 680.45 kB 681.89 kB +0.25% 147.36 kB 147.73 kB
oss-stable/react-test-renderer/cjs/react-test-renderer.development.js +0.21% 680.47 kB 681.92 kB +0.25% 147.38 kB 147.75 kB
oss-stable-semver/react-art/cjs/react-art.production.min.js +0.21% 84.22 kB 84.40 kB +0.09% 26.19 kB 26.22 kB
oss-stable/react-art/cjs/react-art.production.min.js +0.21% 84.25 kB 84.42 kB +0.09% 26.19 kB 26.22 kB
oss-experimental/react-test-renderer/umd/react-test-renderer.development.js +0.21% 743.10 kB 744.65 kB +0.24% 154.68 kB 155.05 kB
oss-experimental/react-test-renderer/cjs/react-test-renderer.development.js +0.20% 709.29 kB 710.74 kB +0.24% 153.02 kB 153.38 kB
facebook-react-native/react-test-renderer/cjs/ReactTestRenderer-dev.js +0.20% 723.31 kB 724.77 kB +0.22% 154.57 kB 154.92 kB

Generated by 🚫 dangerJS against ec38036

while (attributes.length) {
domElement.removeAttribute(attributes[0].name);
}
setInitialProperties(domElement, tag, rawProps, rootContainerElement);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is no longer being inlined on the main path. Seems unfortunate to duplicate it but maybe necessary


expect(body === testDocument.body).toBe(true);
});

it('should not be able to unmount component from document node', () => {
it('should be able to unmount component from document node, but leaves persistent nodes intact', () => {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was incorrectly described for 5 years. it says you shouldn't be able to do something but it asserts that you can (b/c fiber). I've updated the test description and modified it based on the new constraints of insertion markers for head/body

Comment on lines 492 to 500
export function resetProperties(
domElement: Element,
tag: string,
rawProps: Object,
rootContainerElement: Element | Document | DocumentFragment,
): void {
const attributes = domElement.attributes;
while (attributes.length) {
domElement.removeAttribute(attributes[0].name);
}
setInitialProperties(domElement, tag, rawProps, rootContainerElement);
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concept maybe needs more refinement. What we want is to put the already existing node into a state in which it would have been constructed fresh from React. At the moment this does not deal with listeners including ones set by React on previous Placements.

Practically it isn't common to do many placements for html, head, or body. The most common reasons we'd get here are

  1. failed hydration falling back to client render
  2. client only apps that bind to the document (rare still probably)

Comment on lines +400 to +414
export function getInstanceFromNode(node: HTMLElement): null | Object {
return getClosestInstanceFromNode(node) || null;
}

export function preparePortalMount(portalInstance: Instance): void {
listenToAllSupportedEvents(portalInstance);
}

export function prepareScopeUpdate(
scopeInstance: ReactScopeInstance,
internalInstanceHandle: Object,
): void {
if (enableScopeAPI) {
precacheFiberNode(internalInstanceHandle, scopeInstance);
}
}

export function getInstanceFromScope(
scopeInstance: ReactScopeInstance,
): null | Object {
if (enableScopeAPI) {
return getFiberFromScopeInstance(scopeInstance);
}
return null;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were simply relocated since they were in a section between hydration and test selectors.

Comment on lines 1216 to 1211
export function isPersistentInstance(instance: Instance | Container): boolean {
if (instance.nodeType === ELEMENT_NODE) {
return isPersistentInstanceType(instance.tagName.toLowerCase());
}
return false;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to unify this with isPersistentInstanceType but sometimes I have a type string and other times I have a Container and we can't get the type from a Container in the reconciler so I think we need both :(

precacheFiberNode(internalInstanceHandle, element);
updateFiberProps(element, props);
} else {
element = fallbackInstance;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment the fallback instance already has props set on it but that is technically wasted work almost all the time. we could defer it to here but the HostContext isn't built up in the commit phase so it's potentially harder to do here

@sebmarkbage
Copy link
Collaborator

In terms of naming, I don't think we can use Persistent for this since we have a whole Persistent Mode that means other things and the term comes up a lot of other places. So this would have to be called something else.

function commitReconciliationEffects(finishedWork: Fiber) {
function commitReconciliationEffects(
finishedWork: Fiber,
rootContainerInstance: Container,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out, you don't need this. #25024

@@ -223,7 +227,11 @@ if (supportsMutation) {
let node = workInProgress.child;
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
appendInitialChild(parent, node.stateNode);
if (supportsHydration && node.flags & Placement) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of these are forked on supportsHydration but these don't have anything to do with hydration do they? E.g. I can render into document.body with createRoot and it's still relevant right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A cheat technique for now to opt out in non dom renderers. I can move to it’s own thing

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this does not depend on concurrent roots

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will all need to go behind a feature flag because it's technically a breaking change. Doesn't this basically implement the whole new <head> semantics already except it also unmounts it?

@@ -138,6 +141,8 @@ const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';

const INSERTION_MARKER = '%';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems to me that all this insertion marker stuff is only needed because the React algorithm uses "insert before" instead of "insert after" - which is partially because that's what the only DOM APIs favored but also because we use linked lists that only go in one direction so we can't easily look backwards.

However, if it wasn't for that we'd have no need for these markers because we'd just insert after the last known node instead. It seems pretty inelegant to go through the hassle of mutating the DOM to insert visible placeholders when the algorithm should just be able to do that by itself.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the algo to not require any mutations. I think it is decent however lmk if the concerns of the speed of reading expando outweight the benefits of the new approach.

One thing I like about it is it also fixes portal insertion stability

@gnoff gnoff changed the title Implement Persistent Host Instances Implement HostSingleton type Aug 11, 2022
@gnoff gnoff force-pushed the persistent-host-instances branch 3 times, most recently from c68a06c to a00d190 Compare August 16, 2022 11:26
@gnoff gnoff closed this Aug 17, 2022
@gnoff gnoff deleted the persistent-host-instances branch August 17, 2022 08:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants