Skip to content

Commit 4623197

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

File tree

5 files changed

+22
-55
lines changed

5 files changed

+22
-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/_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: 19 additions & 18 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__ = (
@@ -79,12 +79,16 @@ def init_shiny_widget(w: Widget):
7979

8080
# By the time we get here, the user has already had an opportunity to specify a model_id,
8181
# so it isn't yet populated, generate a random one so we can assign the same id to the comm
82-
if getattr(w, "_model_id", None) is None:
83-
w._model_id = uuid4().hex
82+
w._model_id = getattr(w, "_model_id", uuid4().hex)
83+
84+
if not isinstance(w._model_id, str):
85+
raise ValueError(
86+
f"Expected widget id to be a string, but got {type(w._model_id)}"
87+
)
8488

8589
# Initialize the comm...this will also send the initial state of the widget
8690
w.comm = ShinyComm(
87-
comm_id=w._model_id, # pyright: ignore
91+
comm_id=w._model_id,
8892
comm_manager=COMM_MANAGER,
8993
target_name="jupyter.widgets",
9094
data={"state": state, "buffer_paths": buffer_paths},
@@ -94,6 +98,14 @@ def init_shiny_widget(w: Widget):
9498
html_deps=session._process_ui(TagList(widget_dep))["deps"],
9599
)
96100

101+
# If we're in a reactive context, close this widget when the context is invalidated
102+
if has_current_context():
103+
ctx = get_current_context()
104+
ctx.on_invalidate(lambda: w.close())
105+
106+
# Remember to close the widget when the session ends
107+
session.on_ended(lambda: w.close())
108+
97109
# Some widget's JS make external requests for static files (e.g.,
98110
# ipyleaflet markers) under this resource path. Note that this assumes that
99111
# we're setting the data-base-url attribute on the <body> (which we should
@@ -111,7 +123,7 @@ def init_shiny_widget(w: Widget):
111123
# everything after this point should be done once per session
112124
if session in SESSIONS:
113125
return
114-
SESSIONS.add(session) # type: ignore
126+
SESSIONS.add(session)
115127

116128
# Somewhere inside ipywidgets, it makes requests for static files
117129
# under the publicPath set by the webpack.config.js file.
@@ -133,21 +145,10 @@ def _():
133145
comm: ShinyComm = COMM_MANAGER.comms[comm_id]
134146
comm.handle_msg(msg)
135147

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-
147148
def _restore_state():
148149
if old_comm_klass is not None:
149150
Widget.comm.klass = old_comm_klass # type: ignore
150-
SESSIONS.remove(session) # type: ignore
151+
SESSIONS.remove(session)
151152

152153
session.on_ended(_restore_state)
153154

@@ -156,7 +157,7 @@ def _restore_state():
156157
Widget.on_widget_constructed(init_shiny_widget) # type: ignore
157158

158159
# Use WeakSet() over Set() so that the session can be garbage collected
159-
SESSIONS = WeakSet() # type: ignore
160+
SESSIONS: WeakSet[Session] = WeakSet()
160161
COMM_MANAGER = ShinyCommManager()
161162

162163

0 commit comments

Comments
 (0)