Bookmarking with Modules
Introduction
Shiny modules provide a way to create reusable, namespaced components in your application. When using bookmarking with modules, each module instance can independently manage its own bookmark state, making it possible to build complex applications with multiple independent components that all support state preservation.
This guide assumes you’re familiar with both Shiny modules and bookmarking concepts. For bookmark techniques such as saving custom values and using lifecycle callbacks, see Advanced Bookmarking.
How Module Bookmarking Works
Each module instance maintains its own bookmark state, automatically namespaced by the module ID. This means:
- Input values within a module are saved with the module’s namespace prefix
- Custom values saved in
state.valuesare also namespaced - Multiple instances of the same module (with different IDs) maintain independent state
- Module bookmark callbacks (
on_bookmark,on_restore, etc.) work the same as in non-module code
Basic Example
Here’s a simple counter module that bookmarks its state:
- Each
counterinstance (counter1andcounter2) maintains its own count - The
countreactive value is saved and restored independently for each module - The
incrementandresetbuttons are excluded from bookmarking in each module instance - Each module’s bookmark state is automatically namespaced
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
# | standalone: true
# | components: [editor, viewer]
## file: app.py
from urllib.parse import parse_qs, urlparse
from shiny import reactive
from shiny.express import app_opts, module, render, session, ui
app_opts(bookmark_store="url")
@module
def counter_module(input, output, session, initial_value=0):
count = reactive.value(initial_value)
## Module bookmarking
# Exclude the module's increment, reset buttons from bookmarking
session.bookmark.exclude.append("increment")
session.bookmark.exclude.append("reset")
# Update url when count changes
@reactive.effect
@reactive.event(count, ignore_init=True)
async def _():
await session.bookmark()
# Save custom module state
@session.bookmark.on_bookmark
async def _(state):
state.values["count"] = count()
# Restore custom module state
@session.bookmark.on_restore
def _(state):
if "count" in state.values:
count.set(state.values["count"])
## /Module bookmarking
with ui.card():
ui.input_action_button("increment", "Increment")
ui.input_action_button("reset", "Reset")
@render.text
def display():
return f"Count: {count()}"
@reactive.effect
@reactive.event(input.increment)
def _():
count.set(count() + 1)
@reactive.effect
@reactive.event(input.reset)
def _():
count.set(0)
# Use the module
counter_module("counter1")
counter_module("counter2")
app_url = reactive.value(None)
@session.bookmark.on_bookmarked
def _(url):
app_url.set(url)
ui.h3("Bookmarked Query Parameters")
@render.code
def _():
url = app_url()
if not url:
return "(adjust counter to generate a bookmark URL)"
# Parse the URL
parsed = urlparse(url)
query_params = parse_qs(parsed.query)
if not query_params:
return "(none)"
txt = ""
for key, values in query_params.items():
for value in values:
txt += f"{key} = {value}\n"
return txt
Namespacing Behavior
When you save custom values in a module, they’re automatically prefixed with the module’s namespace. For example:
from shiny.express import module
from shiny import reactive
@module
def my_module(input, output, session):
value = reactive.value(42)
@session.bookmark.on_bookmark
async def _(state):
# Saved as "module_id-value" in the bookmark
state.values["value"] = value()If this module is instantiated with ID "widget1", the bookmark will contain widget1-value in its state.
Module Communication and Bookmarking
When modules communicate with each other (see Module Communication), bookmark state is still maintained independently. However, you may need to coordinate bookmarking responsibilities across modules:
from shiny.express import module
from shiny import reactive
@module
def data_module(input, output, session):
data_source = reactive.value(None)
@session.bookmark.on_bookmark
async def _(state):
state.values["data_id"] = data_source()
@session.bookmark.on_restore
def _(state):
if "data_id" in state.values:
data_source.set(state.values["data_id"])
return data_source
@module
def visualization_module(input, output, session, data_source):
# Uses data_source from data_module
# Don't need to bookmark data_source - it's already saved in data_module
view_settings = reactive.value({"zoom": 1.0})
@session.bookmark.on_bookmark
async def _(state):
state.values["settings"] = view_settings()
@session.bookmark.on_restore
def _(state):
if "settings" in state.values:
view_settings.set(state.values["settings"])
mod1_data_source = data_module("mod1")
visualization_module("mod1_viz", data_source=mod1_data_source)Best Practices
Namespace awareness - Remember that module state is automatically namespaced. Use consistent key names within modules.
Return values coordination - If a module returns reactive values that other modules depend on, only the source module needs to bookmark that state. Avoid duplicating bookmarked state across modules.
Module independence - Design modules so their bookmark state is self-contained and doesn’t depend on the order of restoration.
Test module instances - When testing bookmarking, create multiple instances of your module to ensure namespacing works correctly and instances don’t interfere with each other.
Always implement bookmarking - When creating reusable module components (especially for packages), implement bookmark handlers even if bookmarking isn’t enabled. This ensures your modules work correctly when users enable bookmarking in their apps.
Complete Example
Here’s a complete example showing multiple modules with bookmarking support:
- Two independent modules (
filterandstats) with their own state - The filter module provides reactive values to the stats module
- The stats module bookmarks its view count
- Filter values are automatically bookmarked (as inputs)
- Each module manages its own bookmark callbacks
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
## file: app.py
from urllib.parse import parse_qs, urlparse
from shiny import Inputs, Outputs, Session, reactive, render
from shiny.express import app_opts, module, session, ui
ui.page_opts(title="Module Bookmarking Example")
app_opts(bookmark_store="url")
# Filter module
@module
def filter_mod(input: Inputs, output: Outputs, session: Session):
with ui.card():
ui.card_header("Filters")
ui.input_slider("min_value", "Minimum", min=0, max=100, value=0)
ui.input_slider("max_value", "Maximum", min=0, max=100, value=100)
ui.input_action_button("reset", "Reset Filters")
session.bookmark.exclude.append("reset")
@reactive.effect
@reactive.event(input.reset)
def _():
ui.update_slider("min_value", value=0)
ui.update_slider("max_value", value=100)
# Return filter values for other modules to use
return {"min": input.min_value, "max": input.max_value}
# Stats module
@module
def stats_mod(input: Inputs, output: Outputs, session: Session, filters):
update_count = reactive.value(0)
with ui.card():
ui.card_header("Statistics")
@render.code
def stats():
return (
""
"Filters:\n"
f" Min: {filters['min']()}\n"
f" Max: {filters['max']()}\n"
"\n"
f"Updates: {update_count()}"
)
# Bookmark the view count
@session.bookmark.on_bookmark
async def _(state):
state.values["updates"] = update_count()
@reactive.effect
@reactive.event(filters["min"], filters["max"], ignore_init=True)
def _():
update_count.set(update_count() + 1)
@session.bookmark.on_restore
def _(state):
if "updates" in state.values:
update_count.set(state.values["updates"])
# Main app UI
ui.p(
"Adjust filters and watch the update count increase. The URL updates automatically."
)
with ui.layout_columns():
# Set up modules
filters = filter_mod("filters")
stats_mod("stats", filters=filters)
app_url = reactive.value(None)
# Set up automatic bookmarking
@session.bookmark.on_bookmarked
async def _(url: str):
app_url.set(url)
await session.bookmark.update_query_string(url)
@reactive.effect
@reactive.event(filters["min"], filters["max"], ignore_init=True)
async def _():
await session.bookmark()
ui.h3("Bookmarked Query Parameters:")
@render.code
def _():
url = app_url()
if not url:
return "(adjust filters to generate a bookmark URL)"
# Parse the URL
parsed = urlparse(url)
query_params = parse_qs(parsed.query)
if not query_params:
return "(none)"
txt = ""
for key, values in query_params.items():
for value in values:
txt += f"{key} = {value}\n"
return txt
See Also
- Modules - Introduction to Shiny modules
- Module Communication - Passing data between modules
- Bookmarking State - Basic bookmarking concepts
- Advanced Bookmarking - Custom state and lifecycle callbacks