Skip to content

Maintain calculation results in session state #740

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
jcheng5 opened this issue Sep 26, 2023 · 8 comments
Open

Maintain calculation results in session state #740

jcheng5 opened this issue Sep 26, 2023 · 8 comments

Comments

@jcheng5
Copy link
Collaborator

jcheng5 commented Sep 26, 2023

@vnijs had a request to have the results of a long-running reactive calculation "persist" even through navigating away and back. In talking this through in person, maybe the equivalent of Shiny for R's bindCache (combined with bookmarkable state) would do it.

@vnijs
Copy link

vnijs commented Sep 26, 2023

@gshotwell mentioned the idea of saving models, which I really liked. Being able to load them back in + show output and create additional plots, diagnostics, etc. would also be really interesting. You could do that with a separate app designed just to evaluate a particular type of saved models. However, to be able to do it in the same estimation app would be really nice (i.e., restore input values, reactive.Calc state, and output).

@liuzj039
Copy link

I encountered the same issue. I would like to ask if there have been any recent developments about the bindCache function. Thank you very much.

@sambrilleman
Copy link

Just curious if there are any updates on this, or people have any workarounds. Is there a Python equivalent of R's bindCache?

My use case is that I have to hit an external web API, and that API request will be the same for each user of the app. So I would prefer to cache it, and include a time component in the key to invalidate the cached result after X minutes. I've read the docs but based on what I can tell there isn't currently this type of functionality supported in Shiny for Python?

@gshotwell
Copy link
Contributor

@sambrilleman I bet you could get this behaviour by using a reactive outside of the server function. This will let all of the users of the app share the same API call, which you could invalidate on a schedule with invalidate_later. Alternatively you could use reactive.poll to call a cheap API call regularly and only do the full API refresh when something changed. Here's an example I think

https://github.com/posit-dev/py-shiny-templates/blob/main/monitor-database/shared.py#L26
https://github.com/posit-dev/py-shiny-templates/blob/main/monitor-database/app-core.py#L4

@sambrilleman
Copy link

Legend! Thanks @gshotwell , we'll give that approach a go 😀 I guess I didn't realise we can use a reactive outside of the context of a server. I guess if it's out there on its own, not part of the server, it just forms its own reactive dependency graph or something like that 🤔 (i.e. only linked to the invalidation/polling like you mention). Interesting that it still fires on start up as part of the App though. I guess because its still called from the server (even if not declared there) so must be called/loaded by the app atleast once. Anyhow ignore me if I'm talking some gibberish, pretty new to Shiny. I'll take a closer look at your example, and we can just copy that behaviour, thanks!

@gshotwell
Copy link
Contributor

The reactive graph actually works the same way, it's just that when you put it outside of the server function it's shared across all users. So it's instantiated once when the app starts up, and all users share that reactive. When it invalidates it also invalidates all of the session objects that depend on it.

The one thing that I'd be careful of with this pattern is altering the global reactive from within a session. I think it's better to just have a rule that global reactives are a kind of one-way trip and send data to sessions, but aren't modified by them.

@sambrilleman
Copy link

sambrilleman commented Sep 19, 2024 via email

@schloerke
Copy link
Collaborator

schloerke commented Mar 14, 2025

Related: Bookmarking demo app from #1870

from starlette.requests import Request

from shiny import App, Inputs, Outputs, Session, reactive, render, ui
from shiny.bookmark import BookmarkState


# App UI **must** be a function to ensure that each user restores their own UI values.
def app_ui(request: Request):
    return ui.page_fluid(
        ui.markdown(
            "Directions: "
            "\n1. Change the radio buttons below"
            "\n2. Refresh your browser."
            "\n3. The radio buttons should be restored to their previous state."
            "\n4. Check the console messages for bookmarking events."
        ),
        ui.hr(),
        ui.input_radio_buttons(
            "letter",
            "Choose a letter (Store in Bookmark 'input')",
            choices=["A", "B", "C"],
        ),
        ui.input_radio_buttons(
            "letter_values",
            "Choose a letter (Stored in Bookmark 'values' as lowercase)",
            choices=["A", "B", "C"],
        ),
        "Selection:",
        ui.output_code("letters"),
    )


def server(input: Inputs, output: Outputs, session: Session):

    # Exclude `"letter_values"` from being saved in the bookmark as we'll store it manually for example's sake
    # Append or adjust this list as needed.
    session.bookmark.exclude.append("letter_values")

    lowercase_letter = reactive.value()

    @reactive.effect
    @reactive.event(input.letter_values)
    async def _():
        lowercase_letter.set(input.letter_values().lower())

    @render.code
    def letters():
        return str([input.letter(), lowercase_letter()])

    # When the user interacts with the input, we will bookmark the state.
    @reactive.effect
    @reactive.event(input.letter, lowercase_letter, ignore_init=True)
    async def _():
        await session.bookmark()

    # Before saving state, we can adjust the bookmark state values object
    @session.bookmark.on_bookmark
    async def _(state: BookmarkState):
        print("Bookmark state:", state.input, state.values, state.dir)
        with reactive.isolate():
            state.values["lowercase"] = lowercase_letter()

    # After saving state, we will update the query string with the bookmark URL.
    @session.bookmark.on_bookmarked
    async def _(url: str):
        print("Bookmarked url:", url)
        await session.bookmark.update_query_string(url)

    @session.bookmark.on_restore
    def _(state: BookmarkState):
        print("Restore state:", state.input, state.values, state.dir)

        # Update the radio button selection based on the restored state.
        if "lowercase" in state.values:
            uppercase = state.values["lowercase"].upper()
            # This may produce a small blip in the UI as the original value was restored on the client's HTML request, _then_ a message is received by the client to update the value.
            ui.update_radio_buttons("letter_values", selected=uppercase)

    @session.bookmark.on_restored
    def _(state: BookmarkState):
        # For rare cases, you can update the UI after the session has been fully restored.
        print("Restored state:", state.input, state.values, state.dir)


# Make sure to set the bookmark_store to `"url"` (or `"server"`)
# to store the bookmark information/key in the URL query string.
app = App(app_ui, server, bookmark_store="url")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants