Reactive calculations

Reactive calculations

Agenda

  • Reactive calculations

  • Your turn

  • Overriding reactivity

  • Side effects

Reactivity scales

  • Every Shiny app uses this pattern
  • Works for dynamic UIs
  • Shiny is lazy

Saving and reusing calculated values

  • So far we’ve been working with shallow reactive graphs
  • Each input is passed to a rendering function which produces an output
  • Input -> Recipe -> Output can produce repetitive, inefficient applications

Important

  • @reactive.calc creates calculations whose results can be used by one or more outputs
  • This adds intermediate nodes to the reactive graph

Reactive Calc example

Identify repetition

    @output
    @render.table
    def df():
        rand = np.random.rand(input.n_rows(), 1)
        df = pd.DataFrame(rand, columns=["col_1"])
        return df

    @output
    @render.plot
    def hist():
        rand = np.random.rand(input.n_rows(), 1)
        df = pd.DataFrame(rand, columns=["col_1"])
        plot = (
            ggplot(df, aes(x="col_1"))
            + geom_histogram(binwidth=0.1, fill="blue", color="black")
            + labs(x="Random Values", y="Frequency", title="Histogram of Random Data")
        )
        return plot

Problems with repetition

Warning

  1. Code is in multiple places
  2. The app is taking the sample twice
  3. The table and graph are not using the same sample!

Reactive calculation to the rescue

    @reactive.calc
    def sampled_df():
        rand = np.random.rand(input.n_rows(), 1)
        df = pd.DataFrame(rand, columns=["col_1"])

    @render.table
    def df():
        return sampled_df()

    @render.plot
    def hist():
        return (
            ggplot(sampled_df(), aes(x="col_1"))
            + geom_histogram(binwidth=0.1, fill="blue", color="black")
            + labs(x="Random Values", y="Frequency", title="Histogram of Random Data")
        )

Reactive calculations

  • Defined with the @reactive.calc decorator
  • Called like other inputs
  • Can read inputs, reactive values, or other reactive calculations
  • Caches its value, so it’s cheap to call repeatedly
  • Adds a node to the reactive graph
    • Discards cached value when upstream nodes invalidate
    • Notifies downstream nodes when it invalidates

Initial state

Slider
sample_df
Table
Plot

Calculate table

Slider
sample_df
Table
Plot

Calculate table

Slider
sample_df
Table
Plot

Calculate sample

Slider
sample_df
Table
Plot

Calculate sample

Slider
sample_df
Table
Plot

Calculate plot

Slider
sample_df
Table
Plot

Calculate plot

Slider
sample_df
Table
Plot

Deep reactive graph

Slider
sample_df
Table
Plot

Slider changes

Slider
sample_df
Table
Plot

Invalidated

Slider
sample_df
Table
Plot

Invalidated

Slider
sample_df
Table
Plot

Recalculate table

Slider
sample_df
Table
Plot

Recalculate table

Slider
sample_df
Table
Plot

Recalculate sample

Slider
sample_df
Table
Plot

Recalculate sample

Slider
sample_df
Table
Plot

Recalculate plot

Slider
sample_df
Table
Plot

Recalculate plot

Slider
sample_df
Table
Plot

Updated

Slider
sample_df
Table
Plot

Your turn

Agenda

  • Reactive calculations

  • Your turn

  • Overriding reactivity

  • Side effects

Your turn

Exercise 2.3 answer

Slider
Sample
Filter
Table
Plot
Text

Overriding reactivity

Agenda

  • Reactive calculations

  • Your turn

  • Overriding reactivity

  • Side effects

Overriding reactivity

  • By default, when an input changes, all outputs that use it will recalculate

Tip

This isn’t always the user interaction you want:

  • Database queries
  • Expensive modeling
  • Grouping multiple filters together
  • Side effects

Important

  • Use @reactive.event to explicitly specify the trigger for an output or calc

Example of reactive event

@reactive.event

from shiny.express import ui, render, input
from shiny import reactive

ui.input_text("input_txt", "Enter text")
ui.input_action_button("send", "Enter")

@render.text
@reactive.event(input.send)
def output_txt():
    return input.input_txt()

Important

  • @reactive.event overrides the usual implicit dependency detection with an explicit trigger
  • It can be applied to rendering functions or to @reactive.calc
  • It tells Shiny to invalidate the object whenever or more inputs change
  • @reactive.event is often used with action buttons or action links

Your turn

Side effects

Agenda

  • Reactive calculations

  • Your turn

  • Overriding reactivity

  • Side effects

What we’ve learned

  • How Shiny re-renders elements
  • How Shiny detects dependencies between elements (inputs and outputs)
  • How to create reusable calculations with @reactive.calc
  • How to explicitly control reactivity with @reactive.event

Is that enough?

  • Shiny has a function ui.show_modal which triggers a modal window.
  • Using what you know so far, how would you call this function?

Return values vs. side effects

  • Inputs, outputs, and calculations all produce values
  • When an input value changes, downstream calculations and outputs might need to have their values updated as well
  • But sometimes we want to execute some code that doesn’t produce a value
    • Doesn’t fit in the categories of “calculation” or “output”

Reactive Effects

  • The @reactive.effect decorator allows you to react to an input without returning a value
  • Usually paired with @reactive.event
@reactive.effect
@reactive.event(input.show)
def toggle_modal():
    m = ui.modal(
        "This is a somewhat important message.",
        title="Click outside the modal to close",
        easy_close=True,
        footer=None,
    )
    ui.modal_show(m)

The rule

Code that produces:

Desired effect Code
an output @render_*
an intermediate value @reactive.calc
only side effects @reactive.effect

Caution

  • Code that produces a value and a side effect: Don’t do this!

  • The “command-query separation” principle

What’s a side effect?

  • Somewhat context dependent
  • Multiple strategies can work
  • If your solution feels complicated and painful, ask whether you should use the other strategy

Your turn