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

Mark the root as animating if any Portal mutates or resizes #32772

Merged
merged 1 commit into from
Mar 31, 2025

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented Mar 28, 2025

Portals and <ViewTransition> are tricky because they leave the React tree. You might think of a Portal's container conceptually as also being part of a React tree but that's not quite how they're modeled today. They're more like their own roots. So instead, of trying to find a conceptual place in the React tree we treat Portals as their own root.

We have two ways of tracking whether an update to a ViewTransition boundary has occurred. Either a DOM mutation has happened within it, or a resize of a child has caused it to potentially relayout its parent. Normally that just follows the tree structure of React, but not when it's a Portal.

When it's a Portal we don't know which DOM parent it might have affected. For all we know it's at the root (and in fact, in most cases that's where Portals go).

With this PR we mark the root as having been affected by a mutation or resize. This means that the whole document will animate and we can't optimize away from it. This ensures that a mutation to the root of a Portal doesn't go unanimated with other things are animating such as its parent.

You can regain this optimization by adding a <ViewTransition> boundary directly inside the Portal itself so it owns its own animation. If that DOM node is also absolutely positioned it doesn't leak.

Conversely this also means that a mutation inside a Portal doesn't affect its React parent so it won't trigger its parent's animation if this was the only thing animating. That could be unfortunate if this container is actually inside the same React parent. However, because this would have been an update we would've marked it for "maybe animating" and updates can't only get their animations cancelled if the root is cancelled, in practice this will actually animate anyway.

In a way that's not contained by a ViewTransition inside the Portal itself.
@react-sizebot
Copy link

Comparing: 8039f1b...90c23e4

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 = 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js = 515.13 kB 515.13 kB = 91.78 kB 91.78 kB
oss-experimental/react-dom/cjs/react-dom.production.js = 6.69 kB 6.69 kB +0.05% 1.83 kB 1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js = 615.31 kB 615.28 kB +0.01% 108.87 kB 108.88 kB
facebook-www/ReactDOM-prod.classic.js +0.11% 647.04 kB 647.74 kB +0.09% 114.37 kB 114.47 kB
facebook-www/ReactDOM-prod.modern.js +0.11% 637.32 kB 638.02 kB +0.09% 112.79 kB 112.89 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 90c23e4

const portal = showModal ? (
createPortal(
<div className="portal">
Portal: {!show ? 'A' : 'B'}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Previously, this would've been thought of as React as being inside the <ViewTransition> in the page and so it would've considered not leaking to the root and so the root animation gets cancelled. But since this is in the root it now doesn't animate this transition. Now we're more pessimistic about that.

By wrapping this <div> in <ViewTransition> the root no longer needs to animate because the mutation is contained within this ViewTransition.

@sebmarkbage sebmarkbage merged commit 95671b4 into facebook:main Mar 31, 2025
243 checks passed
github-actions bot pushed a commit that referenced this pull request Mar 31, 2025
Portals and `<ViewTransition>` are tricky because they leave the React
tree. You might think of a Portal's container conceptually as also being
part of a React tree but that's not quite how they're modeled today.
They're more like their own roots. So instead, of trying to find a
conceptual place in the React tree we treat Portals as their own root.

We have two ways of tracking whether an update to a ViewTransition
boundary has occurred. Either a DOM mutation has happened within it, or
a resize of a child has caused it to potentially relayout its parent.
Normally that just follows the tree structure of React, but not when
it's a Portal.

When it's a Portal we don't know which DOM parent it might have
affected. For all we know it's at the root (and in fact, in most cases
that's where Portals go).

With this PR we mark the root as having been affected by a mutation or
resize. This means that the whole document will animate and we can't
optimize away from it. This ensures that a mutation to the root of a
Portal doesn't go unanimated with other things are animating such as its
parent.

You can regain this optimization by adding a `<ViewTransition>` boundary
directly inside the Portal itself so it owns its own animation. If that
DOM node is also absolutely positioned it doesn't leak.

Conversely this also means that a mutation inside a Portal doesn't
affect its React parent so it won't trigger its parent's animation if
this was the only thing animating. That could be unfortunate if this
container is actually inside the same React parent. However, because
this would have been an update we would've marked it for "maybe
animating" and updates can't only get their animations cancelled if the
root is cancelled, in practice this will actually animate anyway.

DiffTrain build for [95671b4](95671b4)
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