Advanced Bookmarking
Introduction
While basic bookmarking automatically captures input values and updates URLs, many applications need more sophisticated state management. This guide covers advanced bookmarking techniques for complex scenarios.
Before reading this guide, make sure you’re familiar with the concepts in Bookmarking State, including automatic bookmarking, bookmark buttons, and excluding inputs.
What you’ll learn:
- Custom state - Save reactive values and computed state that aren’t tied to inputs
- Lifecycle callbacks - Hook into the bookmarking process at different stages
- Input updates - Programmatically update inputs during restoration
- Server-side storage - Configure persistent storage for large or sensitive data
These techniques are essential for applications with:
- Complex derived state or calculations that need preservation
- Large state that exceeds URL length limits
- Sensitive data that shouldn’t appear in URLs
- File uploads or other data that can’t be encoded in URLs
Saving Custom Values
Sometimes you need to save state that isn’t directly tied to an input. For example, a calculated value, accumulated state, or reactive value. Use the on_bookmark callback to save custom values into the state.values dictionary.
The state.values dictionary works with both URL and server-side bookmarking (configured via the bookmark_store app option). The state.values is serialized after all on_bookmark callbacks have run and unserialized before calling any on_restore callbacks.
from shiny.express import session
from shiny import reactive
total = reactive.value(0)
# Save `total` in custom values
@session.bookmark.on_bookmark
async def _(state):
# In this case, on_bookmark callbacks don't create reactive contexts,
# so total() can be read directly
state.values["total"] = total()
# Restore `total` (when possible)
@session.bookmark.on_restore
def _(state):
if "total" in state.values:
total.set(state.values["total"])
# Other code to alter the value of `total`...In this example:
- The
totalreactive value isn’t tied to an input - We use
@session.bookmark.on_bookmarkto save it tostate.values - We use
@session.bookmark.on_restoreto restore it when the app loads
Updating Inputs During Restoration
Sometimes you need to update one input based on a bookmarked value. Use the on_restore callback:
from shiny.express import input, session, ui
from shiny import reactive
# Exclude this input from automatic bookmarking
session.bookmark.exclude.append("computed_choice")
computed_value = reactive.value()
@reactive.effect
@reactive.event(input.base_choice)
def _():
# Compute derived value
computed_value.set(input.base_choice().lower())
@session.bookmark.on_bookmark
async def _(state):
# Save the computed value
state.values["computed"] = computed_value()
@session.bookmark.on_restore
def _(state):
# Restore and update the UI
if "computed" in state.values:
uppercase = state.values["computed"].upper()
ui.update_radio_buttons("computed_choice", selected=uppercase)Bookmark Lifecycle Callbacks
Shiny provides four callbacks for customizing the bookmark process:
During Bookmarking:
@session.bookmark.on_bookmark- Called before saving state. Use this callback to add custom values tostate.values. The finalstate.valuesvalue is used for the bookmarked state. For URL bookmarking, values are serialized into the URL query string. For server-side bookmarking, values are written to a JSON file on the server.@session.bookmark.on_bookmarked- Called after saving state. Use this to handle the bookmark URL (e.g., update the browser’s address bar or display a modal).
During Restoration:
@session.bookmark.on_restore- Called before the session is fully initialized. Use this to update inputs with restored values.@session.bookmark.on_restored- Called after the session is fully initialized. Use this for operations that require a fully initialized application, such as updating reactive values or input values.
#| '!! 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 shiny.express import app_opts, input, render, session, ui
from shiny import reactive
app_opts(bookmark_store="url")
ui.page_opts(title="Bookmark Lifecycle Example")
ui.markdown(
"""
This example demonstrates all four bookmark lifecycle callbacks.
Change inputs and watch the log to see when callbacks fire.
"""
)
ui.input_text("name", "Your name", value="")
ui.input_slider("age", "Your age", min=1, max=100, value=30)
log_messages = reactive.value([])
def add_log(message: str):
current = log_messages()
log_messages.set(current + [message])
@render.code
def log():
return "\n".join(log_messages())
# Bookmark lifecycle
@session.bookmark.on_bookmark
async def _(state):
add_log("on_bookmark: Preparing to save state")
state.values["timestamp"] = "2024-01-01" # In real app, use datetime
@session.bookmark.on_bookmarked
async def _(url: str):
add_log(f"on_bookmarked: Bookmark created")
await session.bookmark.update_query_string(url)
@session.bookmark.on_restore
def _(state):
add_log("on_restore: Restoring state before session init")
if "timestamp" in state.values:
# This log will only be shown when running the app outside of documentation
add_log(f" Timestamp from bookmark: {state.values['timestamp']}")
@session.bookmark.on_restored
def _(state):
add_log("on_restored: State fully restored")
# Auto-bookmark on input change
@reactive.effect
@reactive.event(input.name, input.age, ignore_init=True)
async def _():
add_log("Input changed, creating bookmark...")
await session.bookmark()
Best Practices
Handle missing values - Always check if a key exists in
state.valuesbefore trying to restore it duringon_restore.Keep custom state minimal - Only save what’s necessary. Large state objects increase bookmark size and restoration time.
Consider serialization - Ensure custom values you save to
state.valuesare JSON-serializable. Complex objects may need conversion.Test lifecycle order - Verify that
on_restorecallbacks run before your app initializes dependent reactive values.Document bookmark requirements - If your app requires server-side bookmarking, clearly communicate this in deployment documentation.
Limitations
Automatic bookmarking of inputs only: Only
inputvalues are automatically bookmarked. You must manually save and restore reactive values and other computed state usingon_bookmarkandon_restorecallbacks.Serialization constraints: Custom values in
state.valuesmust be JSON-serializable. Complex objects like file handles, database connections, or custom classes need special handling or cannot be bookmarked.Lifecycle timing: The
on_restorecallback runs early in the session initialization. Be careful with operations that depend on the full app being initialized - useon_restoredfor those cases.Server storage persistence: Server-side bookmarks require persistent storage. Ensure your hosting environment supports file persistence across deployments and restarts.
Complete Example
Bookmarking features demonstrated in this example:
- Automatic URL updates as inputs change (with visible URL display)
- Bookmarking tab state (via
id="tabs") - Excluding action buttons from bookmarks
- Saving and restoring custom values (reset count)
- Multiple input types and outputs
#| '!! 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 shiny.express import app_opts, input, render, session, ui
from shiny import reactive
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
ui.page_opts(title="Bookmarkable Statistics App")
app_opts(bookmark_store="url")
# Don't bookmark the reset button
session.bookmark.exclude.append("reset")
# Track number of resets (custom state not tied to an input)
reset_count = reactive.value(0)
# Track the current bookmark URL for display
app_url = reactive.value("")
ui.markdown(
"""
Adjust the parameters below. The URL automatically updates
so you can share your configuration.
Latest Bookmarked URL:
"""
)
@render.express
def bookmark_url():
ui.code(app_url())
ui.input_slider("n", "Sample size", min=10, max=500, value=100)
ui.input_select(
"dist", "Distribution", choices=["Normal", "Exponential", "Uniform"]
)
ui.input_checkbox("show_mean", "Show mean line", value=True)
ui.input_action_button("reset", "Reset to defaults")
ui.hr()
with ui.navset_tab(id="tabs"):
with ui.nav_panel("Plot"):
@render.plot
def histogram():
data = generate_data()
fig, ax = plt.subplots(figsize=(8, 5))
ax.hist(data, bins=30, edgecolor="black", alpha=0.7)
ax.set_xlabel("Value")
ax.set_ylabel("Frequency")
ax.set_title(f"{input.dist()} Distribution (n={input.n()})")
if input.show_mean():
mean = np.mean(data)
ax.axvline(
mean,
color="red",
linestyle="--",
linewidth=2,
label=f"Mean: {mean:.2f}",
)
ax.legend()
return fig
with ui.nav_panel("Summary"):
@render.text
def summary():
data = generate_data()
return f"""
Statistics for {input.dist()} distribution:
Sample size: {len(data)}
Mean: {np.mean(data):.4f}
Std Dev: {np.std(data):.4f}
Min: {np.min(data):.4f}
Max: {np.max(data):.4f}
App has been reset {reset_count()} times
"""
with ui.nav_panel("Data"):
@render.data_frame
def data_table():
data = generate_data()
df = pd.DataFrame({"Value": data})
return df.head(100)
@reactive.calc
def generate_data():
"""Generate random data based on selected distribution."""
n = input.n()
dist = input.dist()
if dist == "Normal":
return np.random.normal(0, 1, n)
elif dist == "Exponential":
return np.random.exponential(1, n)
else: # Uniform
return np.random.uniform(-2, 2, n)
@reactive.effect
@reactive.event(input.reset)
def _():
ui.update_slider("n", value=100)
ui.update_select("dist", selected="Normal")
ui.update_checkbox("show_mean", value=True)
reset_count.set(reset_count() + 1)
# Bookmark when inputs change
@reactive.effect
@reactive.event(input.n, input.dist, input.show_mean, ignore_init=True)
async def _():
await session.bookmark()
# Save custom state
@session.bookmark.on_bookmark
async def _(state):
state.values["reset_count"] = reset_count()
# Restore custom state
@session.bookmark.on_restore
def _(state):
if "reset_count" in state.values:
reset_count.set(state.values["reset_count"])
# Update URL bar and display
@session.bookmark.on_bookmarked
async def _(url: str):
await session.bookmark.update_query_string(url)
app_url.set(url) # Store for display
Server-Side Bookmarking
For production applications with larger state or sensitive data, use server-side bookmarking.
Shiny for Python’s server-side bookmarking is already integrated into Posit’s professional products. No need to configure anything if you are using Posit Connect or Posit Workbench.
Shinyapps.io and Posit Cloud Connect, by design, do not support server-side bookmarking as Apps are deployed within a temporary virtual environment. Any files that are saved will be lost when the virtual environment is shut down.
To control where bookmark data is stored, you need to configure the storage location by setting global functions for saving and restoring bookmark directories. Here’s an example using the pathlib module to create a directory for bookmarks:
from pathlib import Path
from shiny.bookmark import set_global_save_dir_fn, set_global_restore_dir_fn
from shiny.express import app_opts
# Configure app for server-side bookmarking
app_opts(bookmark_store="server")
# Define where bookmarks are stored
bookmark_dir = Path(__file__).parent / "bookmarks"
bookmark_dir.mkdir(exist_ok=True)
def save_bookmark_dir(id: str) -> Path:
"""Create and return the directory for saving a bookmark."""
save_dir = bookmark_dir / id
save_dir.mkdir(parents=True, exist_ok=True)
return save_dir
def restore_bookmark_dir(id: str) -> Path:
"""Return the directory for restoring a bookmark."""
return bookmark_dir / id
# Set global defaults
set_global_save_dir_fn(save_bookmark_dir)
set_global_restore_dir_fn(restore_bookmark_dir)
# ... define app_ui and server ...Server bookmarking is designed for hosting environments. In production, ensure you have:
- Persistent storage that survives app restarts
- Appropriate cleanup policies for old bookmarks
- Sufficient disk space for bookmark storage
See Also
- Bookmarking State - Basic bookmarking concepts
- Bookmarking with Modules - Using bookmarking with Shiny modules
- Modules - Organizing code with modules
- Persistent Storage - Saving data between sessions
- Reactivity Patterns - Working with reactive values