ITADN

bug: Dangling reactivity - reactive objects persist after dynamic UI is removed

#2207Openschloerke 创建于 2026-04-03
S
schloerkecommented
## Summary _Hat tip to @khusmann for clearly articulating this problem._ When dynamic UI is removed (via `remove_ui()`, `@render.ui` re-rendering, or toggling visibility), the server-side reactive objects associated with that UI are not cleaned up. This creates three types of dangling state: 1. **Dangling effects** -- `reactive.effect` objects created for dynamic UI keep firing even after the UI is removed from the DOM. 2. **Dangling calcs** -- `reactive.calc` objects persist in memory with stale values. `Calc_` has no `destroy()` method at all. 3. **Dangling input values** -- `input.foo()` still returns the last value for inputs that no longer exist in the DOM. ## Reproduction ![Image](https://github.com/user-attachments/assets/148b8787-507d-49da-9ea5-687233e94108) [**Open the demo app in Shinylive**](https://shinylive.io/py/editor/#code=NobwRAdghgtgpmAXAAjFADugdOgnmAGlQGMB7CAFzkqVQB0wGmIARKCAcwBsBLT5AEpwoxCjwBuPCrmQs4MUnQgBeVWvUbN6pXIUQAzhQBOUKvuQUAFkbhwLudHHOkAZsgYATdtz4cGyGxExcTtDUzsrU2RHI30eQ2QoFyojZA9caBgeYiUAVQBJZHiA+VIQj0QlJQBGLGQAKnq2Tl5+OBcXOFF9RvcAVwAmAAZqgBZkAANA0Qk4LHbO0QnkYkCqD2QXUlT0zOzkAuQAa1t0cxceI18lZFu4EIhE5LhUqztD4psFcs2jUhgLJY7CwAPIAWSwSgGdUazR8-GIUC4xB69X6wzGk2mwTmiORyxicUM5j4yHgCiMMgA7lJLMgwlw7OIkX0nJCIABmGFNbytDhFCDoPoUZDMris1HokbjCZ8IUULAAD2VAAoAJTLQw8LhcEoUPpGAyAuxcKAJMWszbbG63OXC8yREUQUjILjkDgvZBwRXxEWkt6ycHspQACVIVIsLr6+jglQgAFpkABhXjEI7uMBJtZ2AAK7DgXH8FBdUA8G3Q+a4+jqAFERHSKxAC8hLGbEo8oMLSPG+Kt5NQxJwbQsuhQiFA0hlYPsqIq-YLheOIBsJ3jiGkXrMNi4-gDHQL5ezEwAVUgcbh2BgAZUs4cne3XBX8rjcdEG0uNXo6o+Op3Ol18Ig7RFC0nGiF4iQVJRExTbJ0wYIRvgiSxikbAt-DfDFxn0WBkMA5AACNhWQZ0IwDKlb0ZaJKyKcwOHIOZoOQAB1UxiDpAMEOEGYQmQK8KHCZAwXIKRrTAAVPziDw4AIqAjBQKR6VvKlzBjIwQiMeMpNCASqBtfd9FvPouA2VteII2xHmIRl8w2Pp0EI4iqTNCAAHIoIgJhGEYTyIB3f4lL4GQeBgdBthFABBTAiAUDxjLgIhsVmRLqGkowiD6HglH8gFDKCrBpEcElQvCvjtQHGtFWIOB0DEcgql8gBiZB41atr2o6zquu6nrer6nqlGa4S4sZFAGOoptdVce9p3XMhKAHQaWv6lbVrW9b2qUAABWL4qwTKlGktw0K4AB9TL1TjW5bhsfVDWQTKsERIwPBVG1ruux7no8U6gVLF43seD7gduR7SGFeVTtnCg3rAE6oakRkGCAiBWjgZRjyMVk1QId6Qc+ngsGA06gh4chTqIihiwgQH8bpjMvjKOBkYzRCmcBVDKxZ6yzX0U7lAYAiKATLxOE9IWE30AEXDdUx41S5h6eunG8Y+lWgZBx7ifYrojgI0hFVhwzw1O3ZZpZ69lJmrJH3yFnQIxrG4HVumwYh4Vzp4WGzZtknAgYF38a+uSfq2UgUhVN2KEh6HTo02SxBgWG3TxU6wn1fQA8D241Qa7bdsZLA1I0w72gmgs05eDSVWAohwejxd6ScOJyCIeGID6GAiGMEQTnS5BycZkI1Su66R1EU6yD6BbUmUEpSZCLBQJVIZc98j6tpsZcXgK70KDxo7y7OsQKEZS7Veu26DUeFwGDzSbkBAdvO4AX2YPHmo2r-v42j-ZAigAcgAcQADL5GAcgGsAAxKBNYkzHhQMeFC5gkqL3HiKaMYE+Bih4F4Kgp1TQpEjH-TsxYewQD7PASgiQVjgxnl6DSMgYzzQ8HUZiQJHgBhOrREoSFty7j-gGUEYJu7IK-IsP05gAEgmPBuQwfxcBwA2JhD8ikTg1X-FcfgfAjp8CkAWXA7IPqfx-qYsxXU8ab24jieY35RAHzLqQ0gp1exfAHOfDWwNUFzGwUiXB4QCHhCMCqaoa86Y0isPPHiPj9CkEIXADxStbhNipJPOhIo57oLSdPFIWAPQwzVMgAA1MgaoF8QZZKnjPIucAYYpOyZQNef9zEtJaX-FggDQHgKAcmCKICkyILEd4p6SJ1xpVmOYCcoFfgBQDD7bIf9gJ1Ais8V4QIDzEU+KUH4OVPzCNEXYNc9IxA6hbHEjwmdPHNSkOYU0CQyChWFEo0ULI7DsA2F8KAfASScPWd45AHATDoEsEY66zVhlHNbFIkEcjjCkFwOqMktTbwrhFEiLgoLbgmNaTir+liIWjIcW4cZ5RTrwH0DhD0iT8bGFwKPJWYpkBzyWfM4gUM5zqnKV42p19NgMDkFccoKBXIgDFC-Vyit8bemqrVMqjJKCVRlXVCA9L6ZX3ugwGuC4nTh2QIotFzJtRQAIoyXOPl8WpR3gdTxh9WV+2ENSkGPA3DOnnIeY2qTWWOrVTy+6j0PASFpkkj6j10BBuDSDflU4bYHEKMUFCZZqBEEpisIEaZPx6FEqkFRmIAzoIzFy4NDBihalORcLR-J3mfmAi88UdhCS+n0AAQmRoWpJPMKX8wYNDeMzlDS+E2IYHsAlUyts8cG7O9Mwn43VY8R6x4oAcBAb6cN9MtbaqhnvVdRawB2ooHOMdEbroMGPA4UI-xkUDpVFYYoNbpk0lOaLC8I9D1HtrayZQt8wBAh1C6XZ3Cn6VlOh3GAb9CBtpBjSDwVgBZgGqEMIYABSV9StJ2u0JmGiDwNHryPdLDJMoyB7uwoCgAOuNx1JKjjHLdDA1ym3iOgU0uAWZ8DRo7bG5GJ2cfxk0zxVjt5GF3gem1Zc6MBv0IxqACKX0UZur6x4JKlFkubouhJvGN5bzSkJ-eIm3ApyRGnXS0ZvUfVnXysANY7EiihZsS4zyQCVPSTvfJ6oX4WBCk4d+Vzlq4t86tP+uR0B4OQqEVsNgNg9zTJ6WJn5uCkFkrqTNxZUiIkeDGOw4NUjp0Yt5pBxR819KvDCp9jJJmrN4WUJEUpMRqL-A9ILpgB0Bki33TFyBsV+c6wNPjwz0FEvq8FqGJgovBJk3TZqQhSxRJxBuRw28KE8DAuWhI16gQxgsFcc8nobDy0VF0YUZMIDTpBlUjJ4jRwNNyS547wM6OMrnopn65LKVqYauNwQwgIvrNZRskCrzqvjBveYaSeh5HhHMB04BYCIHgJzLkY8V5VXGIYdQJ4RCAw1q2XwmZe51n7I5qpE5upZ2XPewGO5f263Vu1S2UsbWPq0qR-jYm93ftYD3Rym7H1pU1RFFecqCqqq88O0zp1G7WeaqbBpJuFAzVvfxs1XI634hxMEsWeeEXht92QBEoyIo+yNf4BOaSc3pIUJkOQIoOn3sqnDkCIwNJ1tUjgK5UyUBeLsAFBcCA+jXSkFIOgFAgRNe909PGAAfDLjbofUgR4FDg4LXPrq6+m7MImsT4kmZpUy6PI28m1PVE9AP0msMUGAF++GgHJrAdfgwAAujnkAWGPoMEczkhgKBTvcaPcW8XryO8lHQMElnSI0NJNo6MuO-faB3aRN3iNDAh5KIHwqFzFegNV4riBsDRAQAvzVPnmGi-tnL8IMgKBSIYxj5Bm-WTwMWs7xjDDWX8usUfaQrT5cjIjAWoXnMPrPW1iae9w7iSyS+N2h8rYX+cAp0S+Web+YIck6YbYS+Gw-oWunoAAQtAiCAIDWBVpIPwAGE+Hfmdg-oJtdkXngJynficDIHPOvtXpvmdNvpKk6m4HQRJCRqXsAHQXXsAMfnwvXjnpjKyKXhgYJk-teupgru-uzEIuCF6IyFQiKDmuMCmsXC8FpLgnYN4pMjYMgNIrItZMIE2HZOgC2nfo9Evp7CqDGIyKINsJ+p4BII1CADGBSodlgAYCqK5PDJlGkpQF8k2EYK5GqGBknrcIPCfjQQ1B1l1gkd1EtDeEFIkJgEtIkZkUkdAJgJ7DnqGqpmnDobJMEnjDhsUXJNugTETBuqTOTJTNTFUZGmAAbvgidBbJmNmMgPfOhGfh2nzLBhLPGEPiFHJNSPGPBkMGSARPGByGwcDNfg9ITNYOqPPiGssQAKywyTbRJ8S6R2DCQ+7JZkZcqhpNHAzXhVxaHaTHKCRbCpB9IgJHz6AozWR9ABr8CoEDxNjVgFp34t4WaUBXBgT7jqIOT2R4IDpJBEJDxVZD7swlYDqoJSCGIob0z9FdpgA9owBPIbBSzopolqxrHVH1yQwXQMDpzRinTtFgCLFQYwajAIbElRFzawZIjOS4CZzgaeKTr+qBq4KwbwzzQCR8AvAnGeInyMiwZwh8gfYLwomyClBokXA6jGpSkX5VgJRKC8alxuCaHD7ap1zEZEAeEtxHaqpClOazyp6LzLyrx4zkE57DLLx743Z-7UmVjmBzzOmvJ2EF5qgyG3BWJ-62ISK-7RLzAPAwxLKtEwEnQQFlynTwEkQ56Wk5LOb+nFKlJcppnVJSFHY5lAa4I56MEVwgAQBgav7AzNSYy9yJC7EnSk74yNk57umNmH6F5kDUGRHPFYClivTwy4I9ltmek1IwyNmBnVnID5AGAvAigBgFzvB2xWGEzfJzm2FYb2GjhOEMCNSWlBGik-7cnBoUQvDoyCztDbBwA1jLiElBw8DKB8niDnF0z+FeyDkeCLF0wClfrMEuIeAvzDFAYBHCnBFinHlvoYmwYwAzFzEQWobMnICTnI78RyTznrKLlNzqSehqFW7mDoLmAPq6glZvLlawmFh36HwwBQAnCwEn6TzoqyRpgqjoBDmi7AyHzRFITJmvmemtnRIemTTViUFdkl7-HNmjnibfQsVDnN7XQjlCVjksWenIUzrybfF0VIRVkfTwz6kvnXQfmIU6VAYgbKAQBGXXTkHKDkEWVREQCaVMzKDUW0U2F4hcBMVHDKXV5DmIWTnxFZFdZ-xAJugJa3EZxkgiTHHeYBVZEWoCb7RZS6ZhVUknTJlkESEdmqW2guq6rcHiVyZ3RzoYawwAIugtmxkbD6qQh9F3IDHdp7zxg4nrCDoUDDp+I5C0naXXT6IwBenIDAB15cr3FHynScGkixJGDrDXpYB0H6DqhjZJJ8BbA55l7wx8FYZfHMp+SkAdmCFMweAswalX5dUgz6wvQvCTy1U56CzbBpTxhPriwcD3XeDXF9BCxIziTOoEHPIFjrY3XnWaRLWKA+TBqmgWS6hzz+DAB4FggggABqNYLAg1n1bgXxv1l43km5ROJMuxc8+lzRAOfEx4+QICTxuQOYHSxNwCLaKN31K4y4XupAAhYAbelAwhkeUx6NBaINSSWVwMPV1YGApu+N2GhMAaz5clmshMuGnAKov5a1cAuAL8IAYNBYERtla60tjYdh2Nf+RAUF9Vc4z1YsqQLgVI8Y+sJk4pb6otBUi61YAkJqCSktmtdtHADtpR+VNtSxbtDtr0DAlmEitmNg+glQNVvMmJjg8YAw9I1Fpy2JuJ1t3tSsj0Ak7tBU-tXtydtw8iWqWwzNrN+8YAdeX52dH0BtYA+JZa5tlt245ArVegpAEmIgzM8FZdCxGtXGLt6GvtBUnt7d95vdFA-tYAM58o76sY3MtVkdcA0dsd6KFgDVTVp+pdydqd9tGdItZdQNzNI+da9end2dFdVduoWwlAjV5ATdFY1Ud5A9q9Ea996d+GR6e92939NAN9B9x9bdI9ydm9h9Z99df9wDD9X6e93dndu9V9H9r9OcqDZ9r9bdA9OcqDdMj97a-9DAMF8YQwt9D9RD10UFX6FDMd6Ac9Z1aUAQdC0kGwIArDF1-RO+LtPZasJ1eovK86i6y6hgKo9QAtOpORDkc8UUYaQtnsJpVxRga8Z+WJZ6tAWJe8YAL8deQAA) 1. Click **Create Panel** to add 2-3 panels. Each creates a module with an auto-incrementing `reactive.effect`, a `reactive.calc`, and a dynamic `input_text`. 2. Uncheck **Show dynamic UI** on a panel -- the sidebar monitor shows the effect count still incrementing and the input value still present. 3. Click **Remove this panel** -- the entry turns red and shows `[REMOVED]`, but the effect count keeps going up and the input/calc values persist. ## Root Cause The server has no concept of reactive object ownership or scoping tied to UI lifecycle: - `insert_ui()` / `remove_ui()` are purely client-side DOM operations. The server sends a message and forgets about it. - Module servers create reactive objects that register with the session, but nothing tracks which module "owns" which reactive objects. - There is no "reactive scope" that groups reactive objects and can destroy them as a unit. - `Calc_` lacks basic lifecycle management -- no `destroy()`, no `session.on_ended()` callback. ### What IS cleaned up today | Mechanism | How | |-----------|-----| | Output effects when re-registered with same ID | `Outputs.remove()` calls `effect.destroy()` | | All `Effect_` on session end | Each registers `session.on_ended(self.destroy)` | | Client-side input/output bindings | `shinyUnbindAll(el)` before DOM replacement | | Output suspension when hidden | `_manage_hidden()` suspends (but does NOT destroy) | ### What is NOT cleaned up | Mechanism | |-----------| | `reactive.effect` from module server after `remove_ui` | | `reactive.calc` (no `destroy()` method exists) | | Server-side `input.*` values for removed DOM inputs | | Any server-side state tracking for `insert_ui` / `remove_ui` | ## Prior Art ### py-shiny - #399 -- `remove_ui` input values persist. Closed as expected behavior with manual workaround. - The todo list example (`shiny/api-examples/todo_list/app-core.py:93-97`) manually calls `effect.destroy()` and comments that this is needed because `remove_ui` only removes HTML. ### R Shiny (rstudio/shiny) This is a long-standing problem in R Shiny as well: - [rstudio/shiny#2281](https://github.com/rstudio/shiny/issues/2281) -- The canonical issue: `insertUI`/`callModule` to add, `removeUI`/??? to remove. No mechanism to deactivate a module server instance. - [rstudio/shiny#825](https://github.com/rstudio/shiny/issues/825) -- "Reactive subDomains" proposal (2016). Proposes `createSubDomain()` where ending the subdomain destroys all reactive objects created within it. - [rstudio/shiny#2374](https://github.com/rstudio/shiny/issues/2374) -- Request to delete server-side input values on `removeUI()`. - [rstudio/shiny#3812](https://github.com/rstudio/shiny/issues/3812) -- Add hooks around dynamic UI lifecycle. ### Current Workaround Users must manually track every `reactive.effect` and call `.destroy()` on removal. This doesn't scale, doesn't work for `reactive.calc` (no destroy method), and doesn't clean up input values. ## Possible Directions (not prescriptive) - Add `destroy()` to `Calc_` - Reactive scoping / ownership tracking (a la rstudio/shiny#825 subDomains) - Server-side awareness of which inputs/outputs belong to which dynamic UI context - Coordination between `remove_ui` / `@render.ui` and server-side cleanup - Module-level teardown mechanism
1 条评论