Skip to content

Commit b10e675

Browse files
committed
Register cleanup methods when widget gets initialized in a reactive context
1 parent 63d44d9 commit b10e675

File tree

6 files changed

+21
-55
lines changed

6 files changed

+21
-55
lines changed

js/src/comm.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,7 @@ export class ShinyComm {
9696
metadata?: any,
9797
buffers?: ArrayBuffer[] | ArrayBufferView[]
9898
): string {
99-
// Inform the server that a client-side model/comm has closed
100-
Shiny.setInputValue("shinywidgets_comm_close", this.comm_id, {priority: "event"});
99+
// I don't think we need to do anything here?
101100
return this.comm_id;
102101
}
103102

js/src/output.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,6 @@ Shiny.addCustomMessageHandler("shinywidgets_comm_close", (msg_txt) => {
215215
});
216216
});
217217

218-
// At least currently, all widgets must be created within a session scope, so we can
219-
// clear the state (i.e., .close() all the widget models) when the session ends
220-
$(document).on("shiny:disconnected", () => {
221-
manager.clear_state();
222-
});
223-
224218
// Our version of https://github.com/jupyter-widgets/widget-cookiecutter/blob/9694718/%7B%7Bcookiecutter.github_project_name%7D%7D/js/lib/extension.js#L8
225219
function setBaseURL(x: string = '') {
226220
const base_url = document.querySelector('body').getAttribute('data-base-url');

shinywidgets/_comm.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,11 @@ def close(
9797
return
9898
self._closed = True
9999
data = self._closed_data if data is None else data
100-
self._publish_msg(
101-
"shinywidgets_comm_close", data=data, metadata=metadata, buffers=buffers
102-
)
100+
# If there's no active session, we can't send a message to the client
101+
if get_current_session():
102+
self._publish_msg(
103+
"shinywidgets_comm_close", data=data, metadata=metadata, buffers=buffers
104+
)
103105
if not deleting:
104106
# If deleting, the comm can't be unregistered
105107
self.comm_manager.unregister_comm(self)

shinywidgets/_render_widget_base.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,6 @@ def __init__(
7070
self._contexts: set[Context] = set()
7171

7272
async def render(self) -> Jsonifiable | None:
73-
# Before executing user function, get currently active widgets
74-
widgets_before = set(WIDGET_INSTANCE_MAP.keys())
75-
7673
value = await self.fn()
7774

7875
# Attach value/widget attributes to user func so they can be accessed (in other reactive contexts)
@@ -91,16 +88,6 @@ async def render(self) -> Jsonifiable | None:
9188

9289
self._widget = cast(WidgetT, widget)
9390

94-
# Collect widget ids introduced by the render function
95-
widgets_after = set(WIDGET_INSTANCE_MAP.keys())
96-
widgets_diff = widgets_after - widgets_before
97-
98-
# The next time this render context gets invalidated, close widgets introduced
99-
# by the user function. This _could_ be problematic if widgets get hoisted
100-
# out of the render function for use elsewhere, but if you're doing that,
101-
# you're likely to have other problems anyway.
102-
get_current_context().on_invalidate(lambda: close_widgets(widgets_diff))
103-
10491
# Don't actually display anything unless this is a DOMWidget
10592
if not isinstance(widget, DOMWidget):
10693
return None
@@ -226,17 +213,3 @@ def set_layout_defaults(widget: Widget) -> Tuple[Widget, bool]:
226213
widget.chart = chart
227214

228215
return (widget, fill)
229-
230-
231-
# Dictionary of all "active" widgets (ipywidgets automatically adds to this dictionary as
232-
# new widgets are created, but they won't get removed until the widget is explictly closed)
233-
WIDGET_INSTANCE_MAP = cast(dict[str, Widget], Widget.widgets) # pyright: ignore[reportUnknownMemberType]
234-
235-
# Given a set of widget ids, close those widgets. Closing should not only
236-
# remove the widget reference, but also send a message to the client to remove
237-
# the corresponding model from the widget manager.
238-
def close_widgets(ids: set[str]):
239-
for id in ids:
240-
widget = WIDGET_INSTANCE_MAP.get(id)
241-
if widget is not None:
242-
widget.close()

shinywidgets/_shinywidgets.py

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from ._cdn import SHINYWIDGETS_CDN_ONLY, SHINYWIDGETS_EXTENSION_WARNING
2727
from ._comm import BufferType, ShinyComm, ShinyCommManager
2828
from ._dependencies import require_dependency
29-
from ._render_widget_base import WIDGET_INSTANCE_MAP
29+
from ._render_widget_base import has_current_context
3030
from ._utils import is_instance_of_class, package_dir
3131

3232
__all__ = (
@@ -94,6 +94,15 @@ def init_shiny_widget(w: Widget):
9494
html_deps=session._process_ui(TagList(widget_dep))["deps"],
9595
)
9696

97+
# If we're in a reactive context, close this widget when the context is invalidated
98+
if has_current_context():
99+
ctx = get_current_context()
100+
ctx.on_invalidate(lambda: w.close())
101+
102+
# Also close the widget when the session ends (if the widget is already closed, this
103+
# is a no-op)
104+
session.on_ended(lambda: w.close())
105+
97106
# Some widget's JS make external requests for static files (e.g.,
98107
# ipyleaflet markers) under this resource path. Note that this assumes that
99108
# we're setting the data-base-url attribute on the <body> (which we should
@@ -111,7 +120,7 @@ def init_shiny_widget(w: Widget):
111120
# everything after this point should be done once per session
112121
if session in SESSIONS:
113122
return
114-
SESSIONS.add(session) # type: ignore
123+
SESSIONS.add(session)
115124

116125
# Somewhere inside ipywidgets, it makes requests for static files
117126
# under the publicPath set by the webpack.config.js file.
@@ -133,21 +142,10 @@ def _():
133142
comm: ShinyComm = COMM_MANAGER.comms[comm_id]
134143
comm.handle_msg(msg)
135144

136-
# Handle a close message from the client.
137-
@reactive.effect
138-
@reactive.event(session.input.shinywidgets_comm_close)
139-
def _():
140-
comm_id = session.input.shinywidgets_comm_close()
141-
# Close the widget, which unregisters/deletes the comm, and also drops
142-
# ipywidget's reference to the instance, allowing it to be garbage collected.
143-
w_obj = WIDGET_INSTANCE_MAP.get(comm_id)
144-
if w_obj:
145-
w_obj.close()
146-
147145
def _restore_state():
148146
if old_comm_klass is not None:
149147
Widget.comm.klass = old_comm_klass # type: ignore
150-
SESSIONS.remove(session) # type: ignore
148+
SESSIONS.remove(session)
151149

152150
session.on_ended(_restore_state)
153151

@@ -156,7 +154,7 @@ def _restore_state():
156154
Widget.on_widget_constructed(init_shiny_widget) # type: ignore
157155

158156
# Use WeakSet() over Set() so that the session can be garbage collected
159-
SESSIONS = WeakSet() # type: ignore
157+
SESSIONS: WeakSet[Session] = WeakSet()
160158
COMM_MANAGER = ShinyCommManager()
161159

162160

shinywidgets/static/output.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)