Module Communication

Communication Between Modules

Once you start breaking your app into modules, you might wonder how to pass values between your module and the rest of the application. For example, you might want to define an input label in the global application scope and pass that label to the module, or have user interactions with one module affect an output of another module. Since modules are just functions inside a specific namespace, they can take and return both reactive and non-reactive arguments, which gives you a rich set of tools for handling application requirements.

There are four main patterns you should be aware of when building Shiny modules:

  1. Modules that take non-reactive arguments
  2. Passing callbacks to modules
  3. Modules that take reactive arguments
  4. Modules that return reactive arguments

Non-reactive Arguments

The easiest way to communicate with modules is to pass non-reactive arguments to them. This is just like passing an argument to a normal Python function, and allows you to set specific module options. For example, say we wanted a counter module which allowed you to set the label and starting value. To do this, you would first add an argument to the module UI function which sets the button label.

from shiny import module, ui, render, reactive, event, App


@module.ui
def counter_ui(custom_label = "Increment counter"):
    return ui.div(
        {"style": "border: 1px solid #ccc; border-radius: 5px; margin: 5px 0;"},
        ui.h2("This is ", custom_label),
        ui.input_action_button(id="button", label=custom_label),
        ui.output_text_verbatim(id="out"),
    )

Next, you would add an argument to the server function which specifies the starting value for the counter.

@module.server
def counter_server(input, output, session, starting_value = 0):
    count =  reactive.value(starting_value)

    @reactive.effect
    @reactive.event(input.button)
    def _():
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

You can then set the options when you call the module in your app. Note that you always need to provide an id to the module function to define its namespace. Using arguments like this makes your modules much more flexible and allows you to encapsulate some of the logic while maintaining the flexibility that your application needs.

#| standalone: true
#| components: [editor, viewer]
## file: app.py

from shiny import App, ui
from .counter import counter_ui, counter_server


app_ui = ui.page_fluid(
    counter_ui("counter1", "Counter 1"),
    counter_ui("counter2", "Counter 2"),
)


def server(input, output, session):
    counter_server("counter1", starting_value=5)
    counter_server("counter2", starting_value=3)


app = App(app_ui, server)

## file: counter.py
from shiny import module, ui, render, reactive, event
import shiny.experimental as x

@module.ui
def counter_ui(label: str = "Increment counter"):
    return x.ui.card(
        x.ui.card_header("This is " + label),
        ui.input_action_button(id="button", label=label),
        ui.output_text_verbatim(id="out"),
    )


@module.server
def counter_server(input, output, session, starting_value = 0):
    count =  reactive.value(starting_value)

    @reactive.effect
    @reactive.event(input.button)
    def _():
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

Passing multiple UI elements to modules

In addition to passing numeric and string values to modules you can also pass any number of UI elements. This allows you to build layout modules similar to ui.sidebar_layout() which can take arbitrary Shiny elements and arrange them in some fashion. There are two main ways to pass multiple UI elements to a module. First, you can have the module take a list as one of the arguments and pass that list to another container function.

@module.ui
def mod_ui(elements):
    return ui.div(elements)

ui = ui.page_fluid(mod_ui([ui.h1("heading"), ui.p("paragraph")]))

This is convneient because it lets the parent context pass in any number of elements to the module, but requires that you wrap the elements in a list before passing them to the module.

The second method is to have your module take non keyword argument with *args. This is how Shiny’s container functions are designed, and using this pattern lets you to call the module UI just like you would any Shiny function.

@module.ui
def mod_ui(*args):
    return ui.div(*args)

ui = ui.page_fluid(mod_ui(ui.h1("heading"), ui.p("paragraph")))

For example, let’s say we wanted to display two cards, one which displayed a standard table, and the other displaying an arbitrary set of selements. One way we could do this is by writing a module which rendered a table in one card and passed *args to a second card.

#| standalone: true
#| components: [editor, viewer]
## file: app.py
import matplotlib.pyplot as plt
import numpy as np
from .modules import table_cards_server, table_cards_ui
from shiny import App, Inputs, Outputs, Session, render, ui

text_tags = [ui.h1("A heading"), ui.p("Some paragraph text")]
reactive_tags = [
    ui.input_numeric("dots", "Number of points", value=25), ui.output_plot("dot_plot")
]

app_ui = ui.page_fluid(
    table_cards_ui("output_example", reactive_tags),
    table_cards_ui("heading_example", text_tags),
)


def server(input: Inputs, output: Outputs, session: Session):
    @output
    @render.plot
    def dot_plot():
        x = np.random.rand(input.dots())
        y = np.random.rand(input.dots())
        fig, ax = plt.subplots()
        ax.scatter(x, y)
        return fig

    table_cards_server("heading_example")
    table_cards_server("output_example")


app = App(app_ui, server)

## file: modules.py
import pandas as pd
import shiny.experimental as x
from shiny import Inputs, Outputs, Session, module, render, ui, module


@module.ui
def table_cards_ui(*args):
    return ui.row(
        x.ui.layout_column_wrap(
            1 / 2,
            x.ui.card(
                x.ui.card_header("Standard table"), ui.output_table("module_table")
            ),
            x.ui.card(x.ui.card_header("New elements"), *args),
        ),
    )


@module.server
def table_cards_server(input: Inputs, output: Outputs, session: Session):
    @output
    @render.table
    def module_table():
        df = pd.DataFrame({"col1": range(4), "col2": range(4)})
        return df

Passing reactives to modules

Basic modules are a powerful way to clean up your code base, but it can be difficult to integrate them into your app’s reactive framework. For example what if we wanted a global button which reset all of the counters in an application? To accomplish this, we can pass reactive objects and use them inside the module just as you would use them in an app.

Important

It’s important to distinguish between calls to reactive objects like input.n() and the reactive object itself (input.n). While input.n is reactive object, calling input.n() returns the current value that object.

#| standalone: true
#| components: [editor, viewer]
## file: app.py

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


app_ui = ui.page_fluid(
    ui.input_action_button("clear", "Clear counters"),
    counter_ui("counter1", "Counter 1"),
    counter_ui("counter2", "Counter 2"),
)


def server(input, output, session):
    counter_server("counter1", starting_value=5, global_clear=input.clear)
    counter_server("counter2", starting_value=3, global_clear=input.clear)


app = App(app_ui, server)

## file: modules.py
from shiny import App, module, reactive, render, ui
import shiny.experimental as x


@module.ui
def counter_ui(label: str = "Increment counter"):
    return x.ui.card(
        x.ui.card_header("This is " + label),
        ui.input_action_button(id="button", label=label),
        ui.output_text_verbatim(id="out"),
    )


@module.server
def counter_server(input, output, session, global_clear, starting_value=0):
    count =  reactive.value(starting_value)

    @reactive.effect
    @reactive.event(global_clear)
    def clear_all():
        count.set(0)

    @reactive.effect
    @reactive.event(input.button)
    def iterate_counter():
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

While this app looks it’s doing something quite different, it’s actually following the same reactive rules as any other app. When we pass input.clear to the module as the global_clear argument, we can use it inside the module just like we would use any other reactive object. You could retrieve its value with global_clear() or use it with @reactive.event(global_clear) to trigger a side effect. Since all of the module instances are receiving the same reactive, changes to that reactive will cause elements within those modules to update.

Passing callbacks to modules

Another common problem with modules is to change some piece of application state from within the module. One intuitive way to do this is to define a state-modifying function at the application level, and pass that function down to the module. When the function is called within the module code, it will update the global application state.

For example, let’s add a text output that adds up the total number of button clicks for a session. To do this we create a reactive.value and a function which increments that value by one. We then pass this function down to the module and call it whenever the module button is clicked. This updates the reactive.value at the application level.

#| standalone: true
#| components: [editor, viewer]
## file: app.py

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

app_ui = ui.page_fluid(
    ui.output_text("total_counts"),
    ui.br(),
    counter_ui("counter1", "Counter 1"),
    counter_ui("counter2", "Counter 2"),
)


def server(input, output, session):
    global_tally =  reactive.value(0)

    def increment_counter():
        global_tally.set(global_tally() + 1)

    @output
    @render.text
    def total_counts():
        return f"Total counts: {global_tally()}"

    counter_server("counter1", _on_click=increment_counter)
    counter_server("counter2", _on_click=increment_counter)


app = App(app_ui, server)

## file: modules.py
from shiny import App, module, reactive, render, ui
import shiny.experimental as x


@module.ui
def counter_ui(label: str = "Increment counter"):
    return x.ui.card(
        x.ui.card_header("This is " + label),
        ui.input_action_button(id="button", label=label),
        ui.output_text_verbatim(id="out"),
    )


@module.server
def counter_server(input, output, session, _on_click, starting_value=0):
    count =  reactive.value(starting_value)

    @reactive.effect
    @reactive.event(input.button)
    def increment_button():
        _on_click()
        count.set(count() + 1)

    @output
    @render.text
    def out():
        return f"Click count is {count()}"

We could accomplish the same thing by passing the reactive value itself down to the module, and while this works it’s not a great idea. Passing the reactive value creates a tight coupling between the module and the particular context in which it was called. The module would be expecting a particular type of reactive value and wouldn’t work for anything else. Additionally the update logic would be split between the application context and the module which makes it harder to reason about. Passing a callback is more flexible because the module can be used to do a variety of things. For example by passing a different callback you could use the same module in another application which did something else when the button was clicked.

Returning reactives from modules

Just like we can pass reactives to modules and use them inside the module code we can also return reactive objects from modules to use them in the application. For example one common form of dynamic user interface is to populate a drop-down menu based on another drop-down. You might have one menu which let the user select a state, and a second which only showed cities in that state. Since this is such a common component, you might want to extract it into a module so that it could be easily added into other applications.

To do this you can have the module’s server function return one of the reactive objects which are defined in the module. This reactive object can then be used in the application context like any other reactive object.

#| standalone: true
#| components: [editor, viewer]
## file: app.py

from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui
from .modules import city_state_ui, city_state_server

app_ui = ui.page_fluid(city_state_ui("cities"), ui.output_text("selected_city"))


def server(input: Inputs, output: Outputs, session: Session):
    city = city_state_server("cities")

    @output
    @render.text
    def selected_city():
        return f"You selected '{city()}'"


app = App(app_ui, server)

## file: modules.py
import pandas as pd
from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui


@module.ui
def city_state_ui():
    return ([
        ui.input_selectize(
            "state", "State", choices=["NY", "CO", "OR", "MI"], selected="NY"
        ),
        ui.output_ui("cities_ui"),
    ])


@module.server
def city_state_server(input: Inputs, output: Outputs, session: Session):
    data = {
        "state": ["NY", "CO", "OR", "MI"] * 5,
        "city": [
            "New York",
            "Denver",
            "Portland",
            "Detroit",
            "Buffalo",
            "Colorado Springs",
            "Salem",
            "Grand Rapids",
            "Rochester",
            "Aurora",
            "Eugene",
            "Warren",
            "Yonkers",
            "Lakewood",
            "Gresham",
            "Sterling Heights",
            "Syracuse",
            "Fort Collins",
            "Hillsboro",
            "Ann Arbor",
        ],
    }
    df = pd.DataFrame(data)

    @output
    @render.ui
    def cities_ui():
        opts = df[df["state"] == input.state()]["city"].unique().tolist()
        return ui.input_selectize("cities", "Cities", choices=opts, selected=opts[0])

    return input.cities

Multiple returns

Sometimes you may want to retrieve multiple reactive objects from the module context. To do this you can use either a tuple or namedtuple to send multiple reactives from a module to another context. For example if you wanted to retrieve both the city and state reactives from the module could you have the module return both of them with return (input.cities, input.state). This tuple could then be unpacked in the application context with city, state = city_state_server("cities").

#| standalone: true
#| components: [editor, viewer]
## file: app.py

from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui
from modules import city_state_ui, city_state_server

app_ui = ui.page_fluid(city_state_ui("cities"), ui.output_text("selected_city"))


def server(input: Inputs, output: Outputs, session: Session):
    city, state = city_state_server("cities")

    @output
    @render.text
    def selected_city():
        return f"You selected '{city()}' in '{state()}'"


app = App(app_ui, server)

## file: modules.py
import pandas as pd
from shiny import App, Inputs, Outputs, Session, module, reactive, render, req, ui


@module.ui
def city_state_ui():
    return ui.TagList(
        ui.input_selectize(
            "state", "State", choices=["NY", "CO", "OR", "MI"], selected="NY"
        ),
        ui.output_ui("cities_ui"),
    )


@module.server
def city_state_server(input: Inputs, output: Outputs, session: Session) -> tuple:
    data = {
        "state": ["NY", "CO", "OR", "MI"] * 5,
        "city": [
            "New York",
            "Denver",
            "Portland",
            "Detroit",
            "Buffalo",
            "Colorado Springs",
            "Salem",
            "Grand Rapids",
            "Rochester",
            "Aurora",
            "Eugene",
            "Warren",
            "Yonkers",
            "Lakewood",
            "Gresham",
            "Sterling Heights",
            "Syracuse",
            "Fort Collins",
            "Hillsboro",
            "Ann Arbor",
        ],
    }
    df = pd.DataFrame(data)

    @output
    @render.ui
    def cities_ui():
        opts = df[df["state"] == input.state()]["city"].unique().tolist()
        return ui.input_selectize("cities", "Cities", choices=opts, selected=opts[0])

    return (input.cities, input.state)

If your return value has a bit more structure, it’s often good to return a namedtuple. Named tuples are similar to tuples except that they allow you to set specific named attributes, this makes them useful for data validation because if you don’t pass the right attributes to a named tuple it will fail early and loudly.

Conclusion

Modules are the main way to grow and scale your Shiny application code. They let you break up your app into tractable parts, define how those parts communicate with one another, and reuse components across applications. While mastering modules takes quite a bit of time, you can accomplish almost anything with the four patterns listed in this article.