Skip to content

[material-ui][ButtonBase] Fix LazyRipple retaining event objects when ripples are disabled - #49000

Open
dizhurnikita wants to merge 1 commit into
mui:masterfrom
dizhurnikita:fix/lazy-ripple-mounted-leak
Open

[material-ui][ButtonBase] Fix LazyRipple retaining event objects when ripples are disabled#49000
dizhurnikita wants to merge 1 commit into
mui:masterfrom
dizhurnikita:fix/lazy-ripple-mounted-leak

Conversation

@dizhurnikita

Copy link
Copy Markdown

Fixes #48999

LazyRipple.mounted is resolved in mountEffect only when this.ref.current !== null, i.e. only when a TouchRipple actually mounted. With disableRipple (commonly set app-wide via MuiButtonBase.defaultProps), no TouchRipple ever renders, so mounted never settles — but the ripple handlers still run on every mousedown/mouseup/blur (they are skipped only for disableTouchRipple), and each start(event)/stop(event) queues this.mount().then(() => this.ref.current?.stop(...args)) on that forever-pending promise. Every interaction with every ripple-disabled ButtonBase therefore permanently retains its event object; when the event's target/relatedTarget sits in later-removed DOM (a closed Dialog, an unmounted view), the entire detached tree is pinned for the page lifetime.

Live reproduction: https://codesandbox.io/s/s44gdq — 100 clicks retain ~200 MouseEvents, each held via PromiseReactionLazyRipple.mounted; three Dialog open/close cycles retain three detached MuiDialog-container trees.

The fix resolves mounted in mountEffect regardless of whether a ripple rendered. The queued actions are already null-safe (this.ref.current?.start(...)), so with no ripple they settle as no-ops instead of accumulating. When a ripple does render, the ref is attached during commit — before the effect — so the didMount bookkeeping and action forwarding behave exactly as before (covered by the second test).

Adds useLazyRipple.test.tsx (the hook previously had no dedicated tests): a regression test asserting mount() settles when no ripple is rendered, and a test asserting queued actions still reach a mounted ripple's actions object.

@code-infra-dashboard

Copy link
Copy Markdown

Deploy preview

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

Bundle size

Bundle Parsed size Gzip size
@mui/material 🔺+2B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes event retention in LazyRipple when ripples are disabled by ensuring its mount promise always settles.

Changes:

  • Resolves the mount promise even when no TouchRipple renders.
  • Adds regression and action-forwarding tests.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
packages/mui-material/src/useLazyRipple/useLazyRipple.ts Prevents indefinitely queued ripple actions.
packages/mui-material/src/useLazyRipple/useLazyRipple.test.tsx Tests disabled and mounted ripple behavior.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@zannager zannager added the component: ButtonBase The React component. label Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component: ButtonBase The React component.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[material-ui][ButtonBase] Every interaction on a disableRipple button permanently leaks its event object (LazyRipple mounted promise never settles)

3 participants