Skip to content

Conversation

@acdlite
Copy link
Collaborator

@acdlite acdlite commented Nov 10, 2025

Fixes #35000

This PR updates the behavior of Activity so that when it is hidden, it hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior, because it was thought that this concern should be left to the userspace code that manages the portal, e.g. by adding or removing the portal container from the DOM. Depending on the use case for the portal, this is often desirable anyway because the portal container itself is not controlled by React.

However, React does own the contents of the portal, and we can hide those elements regardless of what the user chooses to do with the container. This makes the hiding/unhiding behavior of portals with Activity automatic in the majority of cases, and also benefits from aligning the DOM mutations with the rest of the React's commit phase lifecycle.

The reason we have to special case this at all is because usually we only hide the direct DOM children of the Activity boundary. There's no reason to go deeper than that, because hiding a parent DOM element effectively hides everything inside of it. Portals are the exception, because they don't exist in the normal DOM hierarchy; we can't assume that just because a portal has a parent in the React tree that it will also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and hide any portal that is contained within it, and recursively hide its direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that is set only on fiber paths that contain a HostPortal. This lets us skip over any subtree that does not contain a portal.

Matches the pattern we use for the other traversals in the commit phase.
@meta-cla meta-cla bot added the CLA Signed label Nov 10, 2025
@github-actions github-actions bot added the React Core Team Opened by a member of the React Core Team label Nov 10, 2025
@acdlite acdlite force-pushed the activity-portals-fix branch from b976315 to 17ca31a Compare November 10, 2025 06:28
@react-sizebot
Copy link

react-sizebot commented Nov 10, 2025

Comparing: c83be7d...b748c98

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.11% 1.83 kB 1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js +0.01% 608.07 kB 608.16 kB = 107.69 kB 107.64 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 +0.03% 665.99 kB 666.18 kB = 117.39 kB 117.35 kB
facebook-www/ReactDOM-prod.classic.js = 693.36 kB 693.31 kB = 122.02 kB 121.98 kB
facebook-www/ReactDOM-prod.modern.js = 683.78 kB 683.73 kB = 120.40 kB 120.36 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against b748c98

@acdlite acdlite marked this pull request as ready for review November 10, 2025 06:33
Copy link
Member

@rickhanlonii rickhanlonii left a comment

Choose a reason for hiding this comment

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

Looks good, could we add a test? Also seems risky, is it ok to revert if it breaks the sync or could we add a killswitch for the meta builds?

return;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
Copy link
Member

Choose a reason for hiding this comment

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

Could we omit LegacyHidden from this? Want to limit the risk of breaking changes and if they need this feature they should convert to Activity.

@acdlite
Copy link
Collaborator Author

acdlite commented Nov 10, 2025

Yeah I wrote some tests just forgot to stage the file

@acdlite acdlite force-pushed the activity-portals-fix branch from 17ca31a to 58f2ab4 Compare November 10, 2025 15:27
@acdlite
Copy link
Collaborator Author

acdlite commented Nov 10, 2025

and yes you can revert if it breaks the Meta sync. Assuming it's due to a bug rather than due to the intentional behavior change. If something is relying on the portal not being hidden then I'd prefer not the revert and we should find some other solution for whatever that code is trying to do. (We could revert with a Meta-only flag in the meantime.)

@acdlite acdlite force-pushed the activity-portals-fix branch from 58f2ab4 to f2a23f2 Compare November 10, 2025 15:34
This PR updates the behavior of Activity so that when it is hidden,
it hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior,
because it was thought that this concern should be left to the userspace
code that manages the portal, e.g. by adding or removing the portal
container from the DOM. Depending on the use case for the portal, this
is often desirable anyway because the portal container itself is not
controlled by React.

However, React does own the _contents_ of the portal, and we can hide
those elements regardless of what the user chooses to do with the
container. This makes the hiding/unhiding behavior of portals with
Activity automatic in the majority of cases, and also benefits from
aligning the DOM mutations with the rest of the React's commit
phase lifecycle.

The reason we have to special case this at all is because usually we
only hide the direct DOM children of the Activity boundary. There's
no reason to go deeper than that, because hiding a parent DOM element
effectively hides everything inside of it. Portals are the exception,
because they don't exist in the normal DOM hierarchy; we can't assume
that just because a portal has a parent in the React tree that it will
also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and
hide _any_ portal that is contained within it, and recursively hide
its direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that
is set only on fiber paths that contain a HostPortal. This lets us skip
over any subtree that does not contain a portal.
@acdlite acdlite force-pushed the activity-portals-fix branch from f2a23f2 to b748c98 Compare November 10, 2025 15:37
@acdlite acdlite merged commit 5268492 into facebook:main Nov 10, 2025
239 of 240 checks passed
github-actions bot pushed a commit that referenced this pull request Nov 10, 2025
This PR updates the behavior of Activity so that when it is hidden, it
hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior,
because it was thought that this concern should be left to the userspace
code that manages the portal, e.g. by adding or removing the portal
container from the DOM. Depending on the use case for the portal, this
is often desirable anyway because the portal container itself is not
controlled by React.

However, React does own the _contents_ of the portal, and we can hide
those elements regardless of what the user chooses to do with the
container. This makes the hiding/unhiding behavior of portals with
Activity automatic in the majority of cases, and also benefits from
aligning the DOM mutations with the rest of the React's commit phase
lifecycle.

The reason we have to special case this at all is because usually we
only hide the direct DOM children of the Activity boundary. There's no
reason to go deeper than that, because hiding a parent DOM element
effectively hides everything inside of it. Portals are the exception,
because they don't exist in the normal DOM hierarchy; we can't assume
that just because a portal has a parent in the React tree that it will
also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and hide
_any_ portal that is contained within it, and recursively hide its
direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that
is set only on fiber paths that contain a HostPortal. This lets us skip
over any subtree that does not contain a portal.

DiffTrain build for [5268492](5268492)
github-actions bot pushed a commit that referenced this pull request Nov 10, 2025
This PR updates the behavior of Activity so that when it is hidden, it
hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior,
because it was thought that this concern should be left to the userspace
code that manages the portal, e.g. by adding or removing the portal
container from the DOM. Depending on the use case for the portal, this
is often desirable anyway because the portal container itself is not
controlled by React.

However, React does own the _contents_ of the portal, and we can hide
those elements regardless of what the user chooses to do with the
container. This makes the hiding/unhiding behavior of portals with
Activity automatic in the majority of cases, and also benefits from
aligning the DOM mutations with the rest of the React's commit phase
lifecycle.

The reason we have to special case this at all is because usually we
only hide the direct DOM children of the Activity boundary. There's no
reason to go deeper than that, because hiding a parent DOM element
effectively hides everything inside of it. Portals are the exception,
because they don't exist in the normal DOM hierarchy; we can't assume
that just because a portal has a parent in the React tree that it will
also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and hide
_any_ portal that is contained within it, and recursively hide its
direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that
is set only on fiber paths that contain a HostPortal. This lets us skip
over any subtree that does not contain a portal.

DiffTrain build for [5268492](5268492)
manNomi pushed a commit to manNomi/react that referenced this pull request Nov 15, 2025
This PR updates the behavior of Activity so that when it is hidden, it
hides the contents of any portals contained within it.

Previously we had intentionally chosen not to implement this behavior,
because it was thought that this concern should be left to the userspace
code that manages the portal, e.g. by adding or removing the portal
container from the DOM. Depending on the use case for the portal, this
is often desirable anyway because the portal container itself is not
controlled by React.

However, React does own the _contents_ of the portal, and we can hide
those elements regardless of what the user chooses to do with the
container. This makes the hiding/unhiding behavior of portals with
Activity automatic in the majority of cases, and also benefits from
aligning the DOM mutations with the rest of the React's commit phase
lifecycle.

The reason we have to special case this at all is because usually we
only hide the direct DOM children of the Activity boundary. There's no
reason to go deeper than that, because hiding a parent DOM element
effectively hides everything inside of it. Portals are the exception,
because they don't exist in the normal DOM hierarchy; we can't assume
that just because a portal has a parent in the React tree that it will
also have that parent in the actual DOM.

So, whenever an Activity boundary is hidden, we must search for and hide
_any_ portal that is contained within it, and recursively hide its
direct children, too.

To optimize this search, we use a new subtree flag, PortalStatic, that
is set only on fiber paths that contain a HostPortal. This lets us skip
over any subtree that does not contain a portal.
@ziyak97
Copy link

ziyak97 commented Jan 7, 2026

Is this already out? Will this fix - vercel/next.js#85502 (reply in thread)

If it’s not out any ETA

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.

Activity mode=“hidden” does not hide nested portals

4 participants