Shiny Modules

Introduction

Shiny’s execution model allows you to write large applications that render quickly for the user. However, as your application grows in complexity, your code base can become difficult to understand or maintain. While it’s easy to write a small application without worrying too much about the quality of your code, as your application grows, you need to think more carefully about how your code is organized. 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.

Note

Note that while Shiny modules are analogous to Python modules, they are not the same. Python modules are a generic way to organize objects in a namespace, while Shiny modules are used to encapsulate reactive components in a namespace.

Note

Modules are supported in Shiny Express apps as of version 0.10.0.

Functions in Shiny

One of the most important things you can do to improve your code quality is to extract a piece of logic into a function. Using functions allows you to keep the logic in one place, which makes it easier to understand and harder to make mistakes. Functions also allow you to define local variables within the function’s scope, which helps you avoid naming conflicts with the global environment.

While many people are comfortable using functions in their day-to-day programming activities, they often forget to use them when building user interfaces, but functions are just as powerful in this context as they are in any other. To use functions in your Shiny UI, all you have to do is write a function that returns a valid UI element. For example, let’s imagine you had a bunch of sliders which mostly had the same values:

from shiny.express import ui

ui.input_slider("n1", "N", 0, 100, 20)
ui.input_slider("n2", "N", 0, 100, 20)
ui.input_slider("n3", "N", 0, 100, 20)
ui.input_slider("n4", "N", 0, 100, 20)
ui.input_slider("n5", "N", 0, 100, 20)
ui.input_slider("n6", "N", 0, 100, 20)
from shiny import App, ui

app_ui = ui.page_fluid(
    ui.input_slider("n1", "N", 0, 100, 20),
    ui.input_slider("n2", "N", 0, 100, 20),
    ui.input_slider("n3", "N", 0, 100, 20),
    ui.input_slider("n4", "N", 0, 100, 20),
    ui.input_slider("n5", "N", 0, 100, 20),
    ui.input_slider("n6", "N", 0, 100, 20),
)

app = App(app_ui, None)

This code has a lot of repetition which makes it difficult to manage. For example if we wanted to change the maximum value of all of those sliders we would need to make five changes instead of one. Instead of tolerating this repetition, we can create a function which returns an ui.input_slider, and call that function with different ids. You can use this function in combination with list comprehension to further reduce repetition in your code.

A simple function cleans up your code, but still requires multiple calls to that function.

from shiny.express import ui

def my_slider(id):
    return ui.input_slider(id, "N", 0, 100, 20)

my_slider("n1")
my_slider("n2")
my_slider("n3")
my_slider("n4")
my_slider("n5")

List comprehension allows you to apply a ui-generating function to a list of ids.

from shiny.express import ui

def my_slider(id):
    return ui.input_slider(id, "N", 0, 100, 20)

ids = ["n1", "n2", "n3", "n4", "n5"]

[my_slider(x) for x in ids]

For more complicated functions you can use the zip function to turn multiple lists into a list of tuples which allows you to use list comprehension to generate UI elements.

from shiny.express import ui

def my_slider(id, label):
    return ui.input_slider(id, label + " Number", 0, 100, 20)

numbers = ["n1", "n2", "n3", "n4", "n5"]
labels = ["First", "Second", "Third", "Fourth", "Fifth"]

[my_slider(x, y) for x, y in zip(numbers, labels)]

A simple function cleans up your code, but still requires multiple calls to that function.

from shiny import App, ui

def my_slider(id):
    return ui.input_slider(id, "N", 0, 100, 20)

app_ui = ui.page_fluid(
    my_slider("n1"),
    my_slider("n2"),
    my_slider("n3"),
    my_slider("n4"),
    my_slider("n5"),
)

app = App(app_ui, None)

List comprehension allows you to apply a ui-generating function to a list of ids.

from shiny import App, ui

def my_slider(id):
    return ui.input_slider(id, "N", 0, 100, 20)

ids = ["n1", "n2", "n3", "n4", "n5"]

app_ui = ui.page_fluid(
    [my_slider(x) for x in ids]
)

app = App(app_ui, None)

For more complicated functions you can use the zip function to turn multiple lists into a list of tuples which allows you to use list comprehension to generate UI elements.

from shiny import App, ui

def my_slider(id, label):
    return ui.input_slider(id, label + " Number", 0, 100, 20)

numbers = ["n1", "n2", "n3", "n4", "n5"]
labels = ["First", "Second", "Third", "Fourth", "Fifth"]

app_ui = ui.page_fluid(
    [my_slider(x, y) for x, y in zip(numbers, labels)]
)

app = App(app_ui, None)

Why do we need modules?

Using functions in this way is a great way to improve your application code, but it has two main problems. First, while we’re able to use locally scoped variables within the function, each input or output ID needs to be unique across the entire Shiny application. For example, consider this function which returns two UI elements:

from shiny.express import expressify, input, render, module, ui

@expressify
def io_row():
    with ui.layout_columns():
        ui.input_text("text_input", "Enter text")

        @render.text
        def text_output():
            return f'You entered "{input.text_input()}"'

io_row()
from shiny import App, render, ui

def io_row():
    return ui.layout_columns(
        ui.card(ui.input_text("text_input", "Enter text")),
        ui.card(ui.output_text("text_output")),
    )

app_ui = ui.page_fluid(
    io_row(),
)

def server(input, output, session):
    @render.text
    def text_output():
        return f'You entered "{input.text_input()}"'

app = App(app_ui, server)

The io_row() function works fine in this case, but if you try to use it more than once, your app will not render properly. The reason is that Shiny requires all IDs in the UI to be unique, and if we call this function more than once, there will be several elements with the text_input id and several elements with the text_output id. When that happens, Shiny doesn’t know how to connect particular inputs to particular outputs.

One possible way to address this is to add a prefix argument to our function and append that to the ids of all the returned elements. (Note that this can be done with Shiny Core applications, but is difficult to do with Shiny Express.)

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.

How to use modules

There are two sides to modules: creating them, and using them. Each side can be done with Shiny Express or Shiny Core syntax. If a module is written with Core syntax, it can be used in an Express app, and vice versa. In the examples below, we’ll create and use a module using the same type of syntax, but they can work interchangeably.

To create a module with Shiny Express, use the @module decorator on a function that takes input, output, and session as parameters. It can also accept additional parameters. In the example below, it takes one extra parameter, named placeholder.

Note

The signature of this function, with input, output, and session is the same as a the server function in a Shiny Core app.

To use the module, just call the decorated function with a first parameter which is the ID,

from shiny.express import module, render, ui

# Define a module function, which takes input, output, and session
@module
def io_row(input, output, session):
    with ui.layout_columns():
        with ui.card():
            ui.input_text(f"text_input", "Enter text")
        with ui.card():
            @render.text
            def text_out():
                return f'You entered "{input.text_input()}"'

# Call the module function with two different namespace IDs
io_row("first")
io_row("second")

One thing you might have noticed is that when we call the decorated function, we just pass a namespace ID string to it – we don’t pass in input, output, and session. This is because the @module decorator wraps the function so that those three parameters are hidden from the caller, and requires an ID to be passed in.

For example if your function’s signature looks like this:

@module
def my_module(input, output, session):
    ...

Then after it’s wrapped by the decorator, it looks like this from the outside (but note that id is a positional-only parameter):

def my_module(id):
    ...

If you want your module to take additional parameters, you can just add them on to the function. In the example below, we’ve add another parameter named placeholder.

#| standalone: true
#| components: [editor, viewer]
## file: app.py
from shiny.express import module, render, ui

@module
def io_row(input, output, session, placeholder=""):
    with ui.layout_columns():
        with ui.card():
            ui.input_text(f"text_input", "Enter text", placeholder=placeholder)
        with ui.card():
            @render.text
            def text_out():
                return f'You entered "{input.text_input()}"'


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

io_row("row_1")
# Call with a different ID and pass in argument for placeholder
io_row("row_2", placeholder="Enter second thing")
# We can add more instances programmatically
[io_row(x) for x in extra_ids]

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()}"'

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.

#| standalone: true
#| components: [editor, viewer]
## file: app.py
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)

Since modules allow you to tie UI and Server code together in the same namespace, you can include arbitrarily complex interactions within your module. Anything that you can do in a Shiny app can also be done inside of a module, and modules can themselves call other modules. This allows you to break your app up into building blocks of various sizes, compose those blocks to build different applications, and share them with others.

Conclusion

Whenever you find that your Shiny code is repetitive, you should consider whether it’s worth extracting some logic into a function or a module. This article has gone through the basics of modules to explain what they are, why we need them, and how to include them in your application. Once you start using modules, the natural next question is how to communicate between different modules and the rest of the application. To learn more about that see the module communication article.