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.

Note

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.values are 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 counter instance (counter1 and counter2) maintains its own count
  • The count reactive value is saved and restored independently for each module
  • The increment and reset buttons 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 (filter and stats) 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