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
andserver
logic in their ownnamespace
.A
module namespace
can be thought of as a container for a module’s code, and helps to keep the module’svariables
,functions
, andclasses
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 itsinput
andoutput
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 justfunctions
and so anything you can do with a function you can also do with a module.Modules can take any
argument
, and canreturn
any value to the caller.Modules usually include both
UI
andserver
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.
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 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
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 theglobal_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 aside 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 toinvalidate
andre-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.