Skip to content

Modularize your shiny app using shiny module and R6 class

Shiny app

Shiny is an R package developed by the RStudio company that provides a solution for quick web application development mostly for data science/analysis purpose. Usually not a lot of data scientists have much web knowledge. But when the complexity of your data grows, or when your model has many parameters to tune, being able to visualize on a web app can help you make the decision quickly. Shiny is (sort of) a easy MVC web framework in R, and even some one without any web background can build a simple app very quickly.

In shiny, the ui is defined in the ui object, while the server is defined in the server function as below.

# in 'app.R' file
library(shiny)

# define the UI
ui = fluidPage(
    fluidRow(
        numericInput('x', 'Input x', 0),
        numericInput('y', 'Input y', 0),
        "x + y =",
        textOutput('sum', inline = TRUE)
    )
)

# define the server
server = function(input, output, session){
    output$sum = renderText({
        input$x + input$y
    })
}

shiny::shinyApp(ui, server)

Now a problem is, when your app grows, defining everything together makes the project really hard to maintain. Can you imagine to change a component of an app with thousands of lines of codes in a single script?

Shiny modules

Fortunately, RStudio provided a solution to this, the shiny module. This article on the RStudio website gave a brief yet decent introduction to shiny module (ps. here is another thing I want to complain about shiny. As a web framework, the documentation is just far from sufficient. Only some articles on RStudio website? No.). However, it does not solve the problem of the chaos of the namespace of the app. If you define a function or a variable in a module, this will effect everything else. So, my solution is, the R6 class.

R6 class

So what is R6 class. R has 4 OOP systems, the S3, S4, RC (reference class), and R6 class. Like RC, R6 is also referenced OOP system. And the grammar is very similar to other languages. The example code below is an example of how to define an R6Class.

library(R6)
A = R6Class(
    "A",    # classname, must (or usually?) be the same as the left hand side of R6Class above
    public = list(  
        param1 = NULL,  # define attributes. You can define as many attributes as you wish
        param2 = NULL,  # I like to initialize the attributes with NULL, but you can absolutely initialize them with certain values
        initialize = function(param1 = 1, param2 = 2){
            self$param1 = param1
            self$param2 = param2
        },
        method1 = function(){

        },
        method2 = function(){

        }
    )
)

To create an new instance of the defined R6 class A, use A$new(). The default initializer does not take any arguments, however this can be overwritten using the initalize function. So using the example class above, we use A$new(5, 10). use theself keyword to access the attributes within the class in the class methods. To be noted that, the attributes can also be changed from outside of the class. This is not a problem, but if you would like some of your class attributes to be more secure, you can put them in to the private attributes. To learn more about the R6 class, Handley Wickham has a chapter in his book Advanced R about the R6 class.

Modularize your shiny app with R6 class

With the help of R6 class, we are now able to really modularize our shiny app and also separate the namespaces by defining attributes and funcitons inside the classes. In the example below,we first defined a InfoBox module that takes in a msg variable, and print it out on the screen. And because it's a R6 class module, it can be reused. R6 class also allows your to define functions in side the class (inside the public list). And the beauty of that is you can be absolutely confident that this particular function will never effect any other modules.

# InfoBox.R
InfoBox = R6Class(
    "InfoBox",
    public = list(
        # attributes
        id = NULL,

        # initializer
        initialize = function(id){
            self$id = id
        },

        # UI
        ui = function(){

            # the ns function here will prepend a prefix to all the ids in the app.
            ns = NS(self$id)

            tagList(

                # The id in each UI element must be wrapped in the ns function, in
                # order to be correctly recognized in the server function inside 
                # the module.
                textOutput(ns('text'))
            )
        },

        # server
        server = function(input, output, session, msg){
            output$text = renderText({ msg })
        },

        # call
        call = function(input, ouput, session, msg){
            callModule(self$server, self$id, msg)
        }
    )
)
# app.R
library(shiny); library(R6)

# you can use source("InfoBox.R"), however, using import::here makes your code more clear.
import::here(InfoBox, .from = "InfoBox.R")

App = R6Class(
    "App",
    public = list(
        # attributes
        infoBox1 = NULL,
        infoBox2 = NULL,  

        # initialize
        initialize = function(){
            self$infoBox1 = InfoBox$new("box1")
            self$infoBox2 = InfoBox$new("box2")
        },
        # UI
        ui = function(){
            fluidPage(
                self$infoBox1$ui(),
                tags$hr(),
                self$infoBox2$ui()
            )
        },

        # server
        server = function(input, output, session){
            self$infoBox1$call(msg = "I am groot")
            self$infoBox2$call(msg = "I am Steve Rogers")
        }
    )
)

app = App$new()

shiny::shinyApp(app$ui(), app$server)

Structure your shiny app with R6 module

Now with the R6 shiny module, we can break the shiny app into pieces, put them into separate R scripts, group them up in folders. An example of a modularized shiny app structure is below. Different developers definitly have different style, but as long as it is modularized and stored in certain structure, it definitly improves the developing experience.

my-shiny-app
|-- app.R
+-- global.R
+-- components
    +-- modules
    |   +-- ShinyModule.R
    |   +-- DataTable.R
    |   +-- InfoBox.R
    +-- layout
    |   +-- Header.R
    |   +-- Body.R
    |   +-- Sidebar.R
    +-- pages
        +-- Page1.R
        +-- Page2.R
        +-- Page3.R

Conclusion

With the R6 shiny module, this is now like a morden web framework such as Vue or Django. You may find it to be a little redundent sometimes, but it makes the maintainance much easier.