Shiny Modules

1 Intro

Source: Shiny Modules

Writing modules in Shiny is the best strategy for organizing a large Shiny code base.

With modules, you can break your application into small pieces that can be reasoned about separately and composed to build larger applications.

This article explains the basics of why we need modules and how to use them, and the next article explains how to communicate between modules.

2 Why do we need modules

Modules solve these problems by encapsulating both the UI and server logic in their own namespace.

A module namespace can be thought of as a container for a module’s code, and helps to keep the module’s variables, functions, and classes separate from those in other modules.

This separation prevents naming conflicts and makes the code easier to understand and manage.

A namespace is a unique identifier that Shiny assigns to each instance of a module to keep its input and output IDs separate from the IDs of other instances and from the rest of the Shiny application.

3 How to use modules

3.1 Create modules

At their core, modules are just functions and so anything you can do with a function you can also do with a module.

Modules can take any argument, and can return any value to the caller.

Modules usually include both UI and server elements which work together to encapsulate a part of your application, and the module UI and server work exactly the same way they do in a regular Shiny application.

The UI part of the module is a function which returns UI elements, and is decorated with the @module.ui decorator.

This decorator sets a default module namespace, so each component created by the function has a prefix implicitly added to its ID.

@module.ui
def row_ui():
    return ui.layout_columns(
        ui.card(ui.input_text("text_in", "Enter text")),
        ui.card(ui.output_text("text_out")),
    )

The module server function looks just like a Shiny app server function, except it’s decorated with the @module.server decorator.

@module.server
def row_server(input, output, session):
    @output
    @render.text
    def text_out():
        return f'You entered "{input.text_in()}"'

3.2 Use modules

To use this module in an application, you call the module UI and server functions inside of the application UI and server functions.

Every module call includes an id argument which defines the module’s namespace. This id has two requirements.

  • First, it must be unique in a single scope, and can’t be duplicated in a given application or module definition. If you need to generate many instances of a single module, it is often a good idea to store their ids in a list, and use list comprehension to generate the UI and server instances.

  • Second, the UI and server ids must match. This ensures that the UI and server instances exist in the same namespace, and if the ids don’t match, the UI and server modules will not be able to interact.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]

from shiny import App, module, render, ui

@module.ui
def row_ui():
    return ui.layout_columns(
        ui.card(ui.input_text("text_in", "Enter text")),
        ui.card(ui.output_text("text_out")),
    )

@module.server
def row_server(input, output, session):
    @output
    @render.text
    def text_out():
        return f'You entered "{input.text_in()}"'

extra_ids = ["row_3", "row_4", "row_5"]

app_ui = ui.page_fluid(
    row_ui("row_1"),
    row_ui("row_2"),
    [row_ui(x) for x in extra_ids]
)

def server(input, output, session):
    row_server("row_1")
    row_server("row_2")
    [row_server(x) for x in extra_ids]

app = App(app_ui, server)

4 Module communication

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

Modules that take non-reactive arguments Passing callbacks to modules Modules that take reactive arguments Modules that return reactive arguments

4.1 Non-reactive arguments

Add arg to the module UI function.

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

@module.ui
def counter_ui(custom_label: str = "Increment counter"):
    return ui.card(
        ui.h2("This is ", custom_label),
        ui.input_action_button(id="button", label=custom_label),
        ui.output_code(id="out"),
    )

Add arg to the module server function.

@module.server

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

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

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

You can then pass in values 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.

#| '!! 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 import App, ui
# Note: In a normal Shiny Core app, use an absolute import, as in:
# `from counter import ...`
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

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

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

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

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

Note that in the example above we used the relative import from .counter import ... instead of the absolute import from counter import ....

This is necessary when running multiple Shinylive applications on one web page as we do here, so that different apps do not cause conflicts when importing their own counter modules.

In normal Shiny Core applications, you MUST use the absolute import (relative imports will generally not work with Shiny Core applications).

4.2 Passihng multiple UI elements to modules

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.

This is convenient 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.

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

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

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 elements.

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, 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, output, session):
    @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
from shiny import module, render, ui

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

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

4.3 Passing reactives to modules

Important

It is 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

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

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

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

    @reactive.effect
    @reactive.event(input.button)
    def increment_counter():
        rv_count.set(rv_count() + 1)

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

While this app may look it’s doing something quite different, it’s actually following the same reactive rules as any other app.

When we pass input.clear to each module as the global_clear parameter, 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 object, when that object is invalidated, it will cause elements within those modules to invalidate and re-execute.

4.4 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)

    @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

@module.ui
def counter_ui(label: str = "Increment counter"):
    return ui.card(
        ui.card_header("This is " + label),
        ui.input_action_button(id="button", label=label),
        ui.output_code(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)

    @render.code
    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.

Back to top