Skip to content

Conversation

@jackpope
Copy link
Contributor

@jackpope jackpope commented Mar 19, 2025

This implements getRootNode(options) on fragment instances as the equivalent of calling getRootNode on the fragment's parent host node.

The parent host instance will also be used to proxy dispatchEvent in an upcoming PR.

@react-sizebot
Copy link

react-sizebot commented Mar 19, 2025

Comparing: 19176e3...f86376b

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.js = 6.68 kB 6.68 kB +0.05% 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 517.29 kB 517.29 kB = 92.26 kB 92.26 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.11% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 608.06 kB 608.06 kB = 107.88 kB 107.88 kB
facebook-www/ReactDOM-prod.classic.js +0.12% 653.67 kB 654.45 kB +0.11% 115.18 kB 115.30 kB
facebook-www/ReactDOM-prod.modern.js +0.12% 643.95 kB 644.73 kB +0.11% 113.59 kB 113.71 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.59 kB 2,615.08 kB +26.37% 295.89 kB 373.91 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.59 kB 2,615.08 kB +26.37% 295.89 kB 373.91 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.76 kB 2,615.26 kB +26.37% 295.92 kB 373.94 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.00 kB 2,285.08 kB +22.71% 294.98 kB 361.96 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.00 kB 2,285.08 kB +22.71% 294.98 kB 361.96 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.18 kB 2,285.29 kB +22.71% 295.00 kB 361.99 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB

Significant size changes

Includes any change greater than 0.2%:

Expand to show
Name +/- Base Current +/- gzip Base gzip Current gzip
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.59 kB 2,615.08 kB +26.37% 295.89 kB 373.91 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.59 kB 2,615.08 kB +26.37% 295.89 kB 373.91 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.development.js +34.27% 1,947.76 kB 2,615.26 kB +26.37% 295.92 kB 373.94 kB
oss-stable-semver/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.00 kB 2,285.08 kB +22.71% 294.98 kB 361.96 kB
oss-stable/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.00 kB 2,285.08 kB +22.71% 294.98 kB 361.96 kB
oss-experimental/eslint-plugin-react-hooks/cjs/eslint-plugin-react-hooks.production.js +17.61% 1,943.18 kB 2,285.29 kB +22.71% 295.00 kB 361.99 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.development.js +4.59% 6.54 kB 6.84 kB +2.62% 1.60 kB 1.65 kB
oss-experimental/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB
oss-stable-semver/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB
oss-stable/react-reconciler/cjs/react-reconciler-reflection.production.js +4.52% 5.92 kB 6.19 kB +2.56% 1.60 kB 1.64 kB

Generated by 🚫 dangerJS against f86376b

Copy link
Collaborator

@sebmarkbage sebmarkbage left a comment

Choose a reason for hiding this comment

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

You assume that you don't have to update the parent but at the same time you implement updateFragmentInstanceFiber to take an updated parent. Each time you do that, you have to traverse back. The really bad part is that this happens for every update inside the subtree - not just updates to this fragment itself. So for an update deep inside the tree it has also do a few back traversal for every parent Fragment fiber too. This isn't really an issue for just setting the Fiber but anything more gets costly. However, you don't really need to do this at all if you assume it won't update.

There is however one case where it updates. When it unmounts. Ideally you wouldn't keep using a ref after it has been unmounted but you can still have a handle on it. When you do that on a DOM node and the DOM node is disconnected then you get parent DOM node where it was disconnected.

If the Fragment unmounts but the HostParent doesn't unmount, then the remaining instance of the Fragment shouldn't have that as the Parent anymore. This becomes especially important when you implement dispatchEvent because that shouldn't dispatch the event if the Fragment is unmounted.

I think it would be better to find it during operations like we do for children.

This saves some memory and you don't have to do any work on mount or updates until you need it, nor when it unmounts.

That also solves the problem when it unmounts because you'd traverse the .return pointer back. If it's disconnected, it'll find a null. If you find a HostComponent then that HostComponent might also be disconnected. Like if the fragment and host component was both removed by a parent at the same time. However, then you do get the right semantics because getRootNode() will still get you the top most disconnected element.

The special case is if you hit null on the return path before you hit a HostComponent like if only the Fragment was deleted. In the DOM getRootNode() returns the Element itself in this case. So I think in this case where you reach null return Fiber you should return the FragmentInstance itself since that's the equivalent of the "Element itself".

@jackpope
Copy link
Contributor Author

Moving the traversal to avoid the extra work in update makes sense.

Returning self in the case of a null host parent is simple enough. The other unmount case I'm still having trouble producing in a test.

If you find a HostComponent then that HostComponent might also be disconnected. Like if the fragment and host component was both removed by a parent at the same time.

The problem I'm running into is after removing the fragment and host parent, the instance._fragmentFiber.return is null. So the result is the same whether the host parent is removed with the fragment or not. I'll push up the failing test to show the example. Maybe I'm storing the handles or traversing incorrectly in the unmount case.

});

// @gate enableFragmentRefs
it('returns the topmost disconnected element if the fragment and parent are unmounted', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The assertion here fails because the fragment's return is null after unmounting it with its parent

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ok so this happens because we now have this aggressive pass to clean up the return pointers recursively of the whole unmounted tree.

https://github.com/facebook/react/blob/156f0eca20d37f1f5aa2e0f518489f23684c89de/packages/react-reconciler/src/ReactFiberCommitWork.js#L4360C5-L4360C28

This pass is unfortunate because it slows down unmounts but it's also observable semantics in this case.

It exists as a way to mitigate the cost when users cause memory leaks that point into React. This wouldn't be so much an issue if we changed the data structure to be split between Fiber tree nodes and an Instance because the return pointer would be on the Instance which then doesn't point back own into children so the leak is just proportional to the parent path.

Or if we just lived with people leaking. In either case we'd have the right semantics too.

However, the semantics that you have now is not bad neither. It's just unfortunate because if we changed the implementation details later this would change and we'd actually have to add cost to preserve the current semantics.

@jackpope jackpope merged commit 04bf10e into facebook:main Mar 24, 2025
240 of 241 checks passed
@jackpope jackpope deleted the fragment-refs-getrootnode branch March 24, 2025 14:20
github-actions bot pushed a commit that referenced this pull request Mar 24, 2025
This implements `getRootNode(options)` on fragment instances as the
equivalent of calling `getRootNode` on the fragment's parent host node.

The parent host instance will also be used to proxy dispatchEvent in an
upcoming PR.

DiffTrain build for [04bf10e](04bf10e)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants