Skip to content

[internals] Harden useDisposable React internals read with safeReact - #22986

Merged
rita-codes merged 5 commits into
mui:masterfrom
rita-codes:fix/use-disposable-bundler-internals
Jun 29, 2026
Merged

rita-codes merged 5 commits into
mui:masterfrom
rita-codes:fix/use-disposable-bundler-internals

Conversation

@rita-codes

@rita-codes rita-codes commented Jun 29, 2026 •

Copy link
Copy Markdown
Member

Context

useDisposable reads __CLIENT_INTERNALS_… / __SECRET_INTERNALS_… off the React namespace to detect StrictMode (so the store survives StrictMode's mount → unmount → mount cycle with synchronous dispose).

The original read was direct — React.__SECRET_INTERNALS_…. Bundlers rewrite that into a named import of react, and React 19 doesn't export __SECRET_INTERNALS_…, so the build fails with:

Attempted import error: '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED' is not exported from 'react'

This is the error shipped in @mui/x-internals@9.6.0. #22882 already mitigated it by reading through a local alias (const X = React). The alias works in webpack today, but it's the fragile form Base UI deliberately moved away from in material-ui#41190.

Change

Read the internals off Base UI's SafeReact — a plain clone of the React namespace that @base-ui/utils already exports, and which useDisposable already imports from — instead of off React directly. A property read on a regular named import is never rewritten into a react named import, so the build error can't happen on any bundler.

No behavior change: missing keys are still undefined and fall through to a single, normal dispose.

Initially this PR created its own { ...React } clone; per review it now reuses the existing SafeReact to avoid a second clone of the namespace.

Verification

  • TypeScript clean (@mui/x-internals).
  • Unit tests pass (pnpm test:unit --project x-internals), including the StrictMode double-mount and <Activity> cases.
  • Built the package and bundled the real .mjs in a webpack build against React 19.2.6: the original direct-access form errors with '__SECRET_INTERNALS_…' was not found in 'react', and reading the internals off SafeReact bundles clean (0 errors).

`useDisposable` reads `__CLIENT_INTERNALS_…` / `__SECRET_INTERNALS_…` off the
React namespace to detect StrictMode. The original direct read
(`React.__SECRET_INTERNALS_…`) is rewritten by bundlers into a named import of
`react` that React 19 doesn't export, which is the build break shipped in
9.6.0. mui#22882 mitigated it by reading through a local alias (`const X = React`).
The alias works in webpack today but is the fragile form Base UI moved away
from in material-ui#41190.

Use the `safeReact` pattern instead: clone the namespace into a plain object
(`{ ...React }`, compiled to `_extends({}, React)`). The read goes through a
function-call result the bundler can't trace back to the `react` import, so it
can't be rewritten into a named import on any bundler. No behavior change:
missing keys are still `undefined` and fall through to a single dispose.

Verified the built `.mjs` bundles clean in a webpack build against React 19,
where the direct-access form errors with "`__SECRET_INTERNALS_…` is not
exported from 'react'".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rita-codes
rita-codes requested a review from romgrk as a code owner June 29, 2026 08:12
@rita-codes rita-codes added scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. labels Jun 29, 2026
@rita-codes
rita-codes requested a review from Janpot June 29, 2026 08:17
@code-infra-dashboard

code-infra-dashboard Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Deploy preview

https://deploy-preview-22986--material-ui-x.netlify.app/
QR code for https://deploy-preview-22986--material-ui-x.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 🔺+11B(+0.01%) 🔺+17B(+0.05%)
@mui/x-tree-view-pro 🔺+11B(+0.01%) 🔺+18B(+0.04%)
@mui/x-scheduler 🔺+1B(0.00%) 🔺+1B(0.00%)
@mui/x-scheduler-premium 🔺+1B(0.00%) 🔺+7B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@Janpot Janpot left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Small optimization could be to reuse the safeReact object @base-ui/utils is already creating.

Per review: `@base-ui/utils` already exports a `SafeReact` clone of the React
namespace, and `useDisposable` already imports from `@base-ui/utils`. Read the
internals off that shared clone instead of building a second `{ ...React }`
locally. Same bundler-safety (a property read off a named import is never
rewritten into a `react` named import); one less clone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rita-codes

Copy link
Copy Markdown
Member Author

Small optimization could be to reuse the safeReact object @base-ui/utils is already creating.

Good call, fixed! ✅

@rita-codes rita-codes self-assigned this Jun 29, 2026
@rita-codes
rita-codes merged commit c273e8b into mui:master Jun 29, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: code-infra Involves the code-infra product (https://www.notion.so/mui-org/5562c14178aa42af97bc1fa5114000cd). type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants