Skip to content

[Bug]: Uncaught TypeError in _shinyResizeObserver.ts when dynamic components/modals are removed #2273

Open

Description

@slupczynskim

Component

UI (ui.*)

Severity

P2 - Medium (workaround exists)

Shiny Version

1.6.1+

Python Version

3.12

Minimal Reproducible Example

from shiny import App, render, ui
import matplotlib.pyplot as plt

app_ui = ui.page_fluid(
    ui.input_action_button("open_modal", "Open Modal with Plot"),
)

def server(input, output, session):
    @render.plot
    def my_plot():
        fig, ax = plt.subplots()
        ax.plot([1, 2], [3, 4])
        return fig

    @input.open_modal
    def _():
        ui.modal_show(
            ui.modal(
                ui.card(
                    ui.card_header("Card inside Modal"),
                    ui.output_plot("my_plot"),
                ),
                title="Reproduction Modal",
                easy_close=True
            )
        )

app = App(app_ui, server)

Behavior

When a container that is registered with the ShinyResizeObserver (such as a card, sidebar, or a layout component inside a modal) is removed from the DOM, the observer does not automatically stop watching it.

During the cleanup/removal transition:

  1. The DOM elements are detached/unbound, which clears their associated jQuery data (shinyOutputBinding).
  2. The browser triggers a final ResizeObserver callback on the resizing/disappearing container.
  3. The callback runs query selectors inside the target for .shiny-bound-output and finds the detached elements.
  4. Because the elements are detached/unbound, calling $(el).data("shinyOutputBinding") returns undefined.
  5. Destructuring binding and onResize from undefined throws an uncaught JavaScript error, halting the client-side execution.

Proposed Solution

It seems like the underlying issue is actually in bslib.

A defensive check or fallback should be added in _shinyResizeObserver.ts to safely ignore elements whose bindings have already been torn down or are not yet set:

const outputData = $(el).data("shinyOutputBinding");
if (!outputData) return;
const { binding, onResize } = outputData;
if (!binding || !binding.resize) return;

Or using a fallback:

const { binding, onResize } = $(el).data("shinyOutputBinding") || {};
if (!binding || !binding.resize) return;

Disclaimer: This Bug Report was prepared with the help of Antigravity and manually verified.
I greatly appreciate all the hard work the maintainers have poured into the shiny ecosystem and would not like to add AI slop here 😅

Error Messages (if any)

Uncaught TypeError: Cannot destructure property 'binding' of '$(...).data(...)' as it is undefined.
    at _shinyResizeObserver.ts:81:21
    at NodeList.forEach (<anonymous>)
    at ResizeObserver.<anonymous> (_shinyResizeObserver.ts:78:12)

Environment

Python package: shiny (version 1.6.0 / 1.6.1+)
Underlying UI package: bslib (frontend assets compiled from rstudio/bslib version 0.10.0 / 0.11.0+)

OS: Win10
Browser: Edge 149.0.4022.69

Activity

  1. slupczynskim commented on Jun 13, 2026

    @slupczynskim
    Author

    For anyone also experiencing this issue before it's fixed, add this to the javascript in your frontend.
    For me, the bug triggers when opening/closing multiple modals with interactive contents inside:

        /**
         * HOTPATCH: Fix bslib ResizeObserver destructuring crash
         *
         * WHY:
         * In bslib (used by Shiny for Python/R), an internal `_shinyResizeObserver.ts` monitors size
         * changes on outputs. When a modal containing bound outputs is repeatedly closed/destroyed,
         * the ResizeObserver callback can fire on element removal before/during unbinding.
         * The callback attempts to destructure `binding` from jQuery data:
         *     const { binding, onResize } = $(el).data("shinyOutputBinding");
         * Since the element is being unbound/destroyed, `data("shinyOutputBinding")` returns undefined,
         * causing an uncaught TypeError that wedges the Shiny client.
         */
        (function hotpatchResizeObserver() {
            const patch = (jq) => {
                if (!jq?.fn?.data || jq.fn.data._isPatched) return jq;
                const orig = jq.fn.data;
                jq.fn.data = function(key, ...args) {
                    return (key === "shinyOutputBinding" && args.length === 0) 
                        ? (orig.call(this, key) || {}) 
                        : orig.apply(this, arguments);
                };
                jq.fn.data._isPatched = true;
                return jq;
            };
    
            ['jQuery', '$'].forEach(name => {
                let instance = window[name];
                if (instance) patch(instance);
                try {
                    Object.defineProperty(window, name, {
                        get: () => patch(instance),
                        set: (val) => { instance = patch(val); },
                        configurable: true,
                        enumerable: true
                    });
                } catch (err) {
                    console.warn(`[scripts.js] Failed to intercept window.${name}`, err);
                }
            });
        })();
  2. cpsievert commented on Jun 15, 2026

    @cpsievert
    Collaborator

    When I run your example, this is what I see (btw, input.open_model is not intended to be used as a decorator):

    Traceback (most recent call last):
      File "/lib/python3.12/site-packages/shiny/session/_session.py", line 903, in _run_impl
        self.app.server(self.input, self.output, self)
      File "/home/pyodide/app_uxe4v2b47gzu0z35ugq2/app.py", line 15, in server
        @input.open_modal
         ^^^^^^^^^^^^^^^^
    TypeError: Value.__call__() takes 1 positional argument but 2 were given
    Value.__call__() takes 1 positional argument but 2 were given
    

    Also the latest shiny release (v1.6.3) includes a resize observer fix that might be related to this -- can you reproduce with the latest release?

  3. added
    wrong locationTarget repository or upstream location and reason.
    Priority: HighHigh-confidence regression or severe bug affecting many users or current release work.
    ai-triage:needs-reviewNeeds human review before the workflow takes further action.
    on Jun 22, 2026
  4. ebarkhordar commented on Aug 14, 2026

    @ebarkhordar

    On the version question: the resize fix in 1.6.3 changed IntersectionObserver for nested outputs, not this path. _shinyResizeObserver.ts:81 in bslib main is still the unguarded const { binding, onResize } = $(el).data("shinyOutputBinding"), and no guarded variant appears in any bundle shipped with 1.6.0 through 1.7.0, so upgrading will not move it.

    Rewriting the example with @reactive.effect plus @reactive.event(input.open_modal), I could not trigger the error: headless Chromium, shiny 1.6.0 and 1.6.1, six plot outputs per modal, 40 open/close cycles over four close paths. That neither confirms nor rules out the report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Priority: HighHigh-confidence regression or severe bug affecting many users or current release work.ai-triage:needs-reviewNeeds human review before the workflow takes further action.bugSomething isn't workingneeds-triagewrong locationTarget repository or upstream location and reason.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions