Understanding Reactivity in Shiny For Python

Ahmed Mamdouh
9 min readNov 24, 2023
Reactivity

It's been almost a year now since Py-shiny was introduced to the Python community, and I think it’s starting to pick up with other Python frameworks (Streamlit, Dash, etc.).

Therefore, in order to assist the Python community in becoming more fluent with Shiny, I’ve decided to start posting about how I understand Pyshiny concepts and try to explain them as clearly as possible, both conceptually and by using code examples.

Today’s topic is reactivity and reactive programming, one of the core concepts that distinguish Shiny from its competitors (check out Gorden Shotwell’s article that describes How PyShiny is Different).

Now, do you remember Newton’s third law, which states

every action has an equal and opposite reaction

So it’s logical to say that any reaction always has a specific action that causes it to happen first, right?

Okay, now back to reactive programming. In reactive programming, we write code to react when two very fundamental cases happen:
1. A change in the data itself (state)
2. A change in an event (functionality or an action that happened)

You can think of reactive programming as a design mindset that you have to have when writing code.

In this mindset, you write an interactive application that reacts when a user changes data in the app or does some interaction like clicking a button, scrolling down the page, etc.

Shiny behind the scenes does this automatically for you; you just have to take the wheel and tell it what to react to.

So you say to Shiny if the user did change that (event or data), then do that (functionality) in response.

Shiny behind-the-scenes workflow

When a Shiny app starts, Shiny constructs what’s called a reactive graph, which is, in simple terms, a graph that describes a chain of dependent executions between the input and output Shiny components.

It uses this directed graph to track changes that would happen in the app state.

Simple Reactive Graph

When a user of the Shiny app changes something (data changes) in an input UI component, Shiny uses the reactive graph to invalidate only the dependent executions of that changed input UI component.

Hence, the performance boost comes from lazy loading, where Shiny renders only a part of the app, not the whole app as other frameworks do.

In brief, what happens during the invalidation process?

As stated in Mastering Shiny by Hadley Wacom (an excellent book), the process consists of the following three steps:

  1. Invalidating the changed component in the reactive graph
Step1

2. Notify or tell all the dependencies of that UI input to be invalidated.

Step2

3. Remove all the relationships between the Input UI component and its dependencies

Step3

Then, after the invalidation is finished, Shiny re-executes and figures out the new relationships between components again by itself.

Reactivity Controls

Now that we sort of grasp what reactivity means and how Shiny knows whenever the user performs UI input changes, let’s learn how to use reactivity in practice.

PyShiny provides a set of functionality in the form of decorators to help you easily control reactivity in your apps.

Let’s take a look at the four key functions that provide us with that kind of power, as listed below:

  • @reactive.Calc()
  • @reactive.event()
  • @reactive.Effect()
  • @reactive.Value() function

But, first and foremost, what is a Decorator?

For people who don't have that Python experience, I find it worth mentioning, in short, the Python decorator construct, since shiny reactivity is highly dependent on such a feature.

Photo by freepik

Python decorators can be thought of as a tool belt that allows you to enhance or modify the behavior of functions. Just as a carpenter may use different tools to modify the shape or size of a piece of wood, decorators can be used to modify the behavior of a function without changing its code.

With a tool belt, a carpenter can quickly switch between tools to perform different tasks, and with decorators, you can quickly modify the behavior of one or more functions. Just as a tool belt is a versatile and essential tool for a carpenter, decorators are a versatile and essential tool for any Python developer.

Let’s see that tool belt in action with shiny decorators.

@reactive.Calc() Decorator

We use reactive calculations as an intermediate calculation step to ensure the principle of Don’t Repeat Yourself (DRY) when we write code, and also to prevent the app from consuming unnecessary calculation resources (CPU/RAM) for calculating the same thing over and over again.

So instead of making shiny apps with this kind of reactive graph,

We make it like this.

Let’s understand this better with an example.

Assume your manager has asked you to create a shiny app that filters the penguin dataset and then displays two outputs of that filtered data: one as a text using the output_text_verbatim component and the other as a visual table using the output_data_frame component.

He also asked you not to calculate the filtering every time, so how will you do it?

Yup, you guessed it right!

By using the reactive.Calc decorator, here is the code for the app.

from shiny import App, render, ui,reactive
import palmerpenguins


penguins = palmerpenguins.load_penguins()
app_ui = ui.page_fluid(
ui.h2("Hello Shiny!",style="background-color:orange; color:white; "),

ui.input_slider("n", "Input the Year:", 2007, 2009, 1000),
ui.output_text_verbatim("filtered_txt"),
ui.output_data_frame("grid"),

)


def server(input, output, session):
@reactive.Calc
def func1():
return penguins.loc[( penguins['species'].isin(['Chinstrap','Adelie']) ) & (penguins.year == int(input.n()))]

@output
@render.text
def filtered_txt():
return func1()

@output
@render.data_frame
def grid():

return render.DataGrid(
func1(),
height="100%",
width="100%",
)


app = App(app_ui, server)

ShinyLive app link

It is worth mentioning that the reactive.Calc decorator forces the reactive graph to be invalidated or re-evaluated when it observes a change in the shiny input component that is used in its body.

It's like telling Shiny to watch for any changes in the inputs, and then, after it notices them, it should react by calculating this input and its dependencies by re-constructing the reactive graph.

@reactive.Effect Decorator

reactive.Effect is a side effect, and a function is called a side effect when a function relies on or modifies some state outside its parameters, like changing the value of a variable or writing some data to disk, etc.

So basically, you’re modifying something in the outer scope of your function, and after you’ve done with that modification, you force the reactive graph to take a dependency on that same modification and then to be re-evaluated when any one of the shiny inputs changes based on that modification, but without returning a value.

Remember that reactive.Calc is an intermediate calculation function, so it has to return a value, and it has to modify its parameters; therefore, if you want to force the re-evaluation of the reactive graph without an intermediate value, use reactive.Effect.

It’s best used when you want to modify the Shiny UI reactively, as in

  • Using the update functions group (ui.update_text, ui.update_slider, ui.update_numeric, etc).
  • Inserting and deleting UI elements using ui.insert_ui(), and ui.remove_ui() based on custom logic.
  • Changing the UI via a reactive.Value().

Here’s an example to showcase the first point, where we insert values on every slider input change.

from shiny import App, render, ui, reactive

app_ui = ui.page_fluid(
ui.h5("Hover over the number to see it's multiplied value."),
ui.input_slider("n", "List of the Inserted Numbers: [", 0, 100, 20),
ui.output_text_verbatim("txt"),
)


def server(input, output, session):
@reactive.Effect
def _():
ui.insert_ui(
ui.tooltip(
f"{input.n()}, ", ui.h1(f"Multiple of n={input.n()*2}")
),
".irs--shiny",
where="beforeBegin",
)

@output
@render.text
def txt():
return f"n*2 is {input.n() * 2}"



app = App(app_ui, server)

Shinylive Link

@reactive.event Decorator

Before calculating or executing an action, you may wish to wait for a specific action from the user, such as clicking a button; hence, we use reactive.event when we want to enforce an event to happen after the users of our apps take a certain action.

This tells Shiny to remove the relationship in the reactive graph between the input and any other reactive function or values that depend on it and only react to this specific event when it occurs.

Consider it a method of limiting execution to only regenerating the reactive graph when a user conducts an event. Therefore, it should always be used before the last two decorators (@reactive.Calc, @reactive.Effect).

Here’s a very good example from the documentation of using @reactive.event, In this example, we've got 3 function types that use reactive.event:

  1. a function to render the value into the UI
  2. a function that uses reactive.event to trigger a reactive.Calc as intermediate function
  3. a function that uses reactive.event to trigger an insert-to-UI operation (which is an outer scope operation) without returning a value using reactive.Effect.
import random

from shiny import App, Inputs, Outputs, Session, reactive, render, ui

app_ui = ui.page_fluid(
ui.markdown(
f"""
This example demonstrates how `@reactive.event()` can be used to restrict
execution of: (1) a `@render` function, (2) `@reactive.Calc`, or (3)
`@reactive.Effect`.

In all three cases, the output is dependent on a random value that gets updated
every 0.5 seconds (currently, it is {ui.output_ui("number", inline=True)}), but
the output is only updated when the button is clicked.
"""
),
ui.row(
ui.column(
3,
ui.input_action_button("btn_out", "(1) Update number"),
ui.output_text("out_out"),
),
ui.column(
3,
ui.input_action_button("btn_calc", "(2) Show 1 / number"),
ui.output_text("out_calc"),
),
ui.column(
3,
ui.input_action_button("btn_effect", "(3) Log number"),
ui.div(id="out_effect"),
),
),
)


def server(input: Inputs, output: Outputs, session: Session):
# Update a random number every second
val = reactive.Value(random.randint(0, 1000))

@reactive.Effect
def _():
reactive.invalidate_later(0.5)
val.set(random.randint(0, 1000))

# Always update this output when the number is updated
@output
@render.ui
def number():
return val.get()

# Since ignore_none=False, the function executes before clicking the button.
# (input.btn_out() is 0 on page load, but @@reactive.event() treats 0 as None for
# action buttons.)
@output
@render.text
@reactive.event(input.btn_out, ignore_none=False)
def out_out():
return str(val.get())

@reactive.Calc
@reactive.event(input.btn_calc)
def calc():
return 1 / val.get()

@output
@render.text
def out_calc():
return str(calc())

@reactive.Effect
@reactive.event(input.btn_effect)
def _():
ui.insert_ui(
ui.p("Random number!", val.get()),
selector="#out_effect",
where="afterEnd",
)


app = App(app_ui, server)

Shinylive link

reactive.Value() Function

So far, we have discussed how to make a reactive function that only observes users' input changes using the last three decorators.

Now we move on to how we, as developers, actively create an object that controls the reactive workflow.

@reactive.Value is an object that can store any type of data, and when its value is changed, it invalidates the dependencies that depend on it. That’s exactly what happens when you use shiny inputs like(input.x) in a shiny app, so shiny inputs behind the scenes are reactive values.

Here’s an example of how to use a reactive value to toggle between hiding and showing a complete counter module by simply hitting a button.

from shiny import App, render, ui, reactive,module
from .counter import counter_ui, counter_server

app_ui = ui.page_fluid(
ui.input_action_button("toggle", "Toggle UI"),
ui.output_ui("toggle_ui"),

)

def server(input, output, session):
"""
The reactive variable triggers the switching behavior
(from true to false and vice versa).
"""
x = reactive.Value(True)

counter_server("counter1")


@reactive.Effect
@reactive.event(input.toggle)
def _():
x.set(not x())

@output
@render.ui
def toggle_ui():
app_ui = ui.panel_conditional(
str(x()).lower(),counter_ui("counter1"),

)
return app_ui

app = App(app_ui, server)

Shinylive link for checking the app live.

Conclusion

Understanding reactivity will offer you more control over the interactive elements in your Shiny projects. It’s really just one of those things that you need to play around with until it becomes intuitive.

We’ve talked about just four of the reactive controls, but there are many more that could give you more capabilities for different scenarios. For instance, the reactive.poll decorator is mostly used to watch for external data changes, as in databases or files.

Feel free to wander through the documentation, as the Pyshiny team has done an excellent job at simplifying concepts.

That’s it! I hope you found this post helpful! If you have any questions or would like to request a tutorial, please don’t hesitate to reach out to me at pevolution.ahmed@gmail.com, Happy Shining!

--

--

Ahmed Mamdouh

a Data enthusiastic person who is passionate about learning and sharing what I have learned with others