Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ function provideModule(moduleName: string, module: any) {
}

function invalidateModule(moduleName: string) {
// next requestModule waits for a fresh provideModule (hot reload); called
// synchronously on change:code so consumers created after the update never
// see the stale module
// Next requestModule waits for a fresh provideModule. This is called
// synchronously for both replacement models and trait updates so consumers
// created during hot reload never see the stale module.
delete modules[moduleName];
delete moduleFunctions[moduleName];
}
Expand Down Expand Up @@ -315,6 +315,10 @@ export class Module extends WidgetModel {
}
initialize(attributes: any, options: any): void {
super.initialize(attributes, options);
// A kernel restart replaces the old Module model instead of updating its
// code trait. Invalidate synchronously so consumers created alongside the
// replacement wait for this model rather than using the cached module.
invalidateModule(this.get("name"));
this.addModule();
// hot reload: re-import when the kernel ships new module code
this.on("change:code change:url change:dependencies", () => {
Expand Down
21 changes: 21 additions & 0 deletions tests/ui/module_hot_reload_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,24 @@ def test_module_code_hot_reload(solara_test, page_session: playwright.sync_api.P
ipyreact.define_module("hot-reload-module", code_template % 2)
display(ipyreact.ValueWidget(_module="hot-reload-module", _type="Label"))
page_session.locator(".hot-widget >> text=version 2").wait_for()


def test_module_recreated_widget_hot_reload(solara_test, page_session: playwright.sync_api.Page):
# A *new* module widget for a name that is already registered must win
# over the module the registry still holds. Solara closes the per-kernel
# module widgets on a hot reload (a trait update on a closed widget never
# reaches the browser) and creates fresh ones, so without invalidating,
# consumers rendered afterwards keep resolving the previous module.
from solara.server import esm, kernel_context

ipyreact.define_module("recreate-module", code_template % 1)
display(ipyreact.ValueWidget(_module="recreate-module", _type="Label"))
page_session.locator(".hot-widget >> text=version 1").wait_for()

# what context.restart() does to the module widgets on a hot reload
kernel_id = kernel_context.get_current_context().id
esm._modules_added_per_kernel[kernel_id]["recreate-module"].close()

ipyreact.define_module("recreate-module", code_template % 2)
display(ipyreact.ValueWidget(_module="recreate-module", _type="Label"))
page_session.locator(".hot-widget >> text=version 2").wait_for()
Loading