Mwavu
Mwavu

Mwavu

Introduction To R Shiny Modules

Photo by Volodymyr Hryshchenko on Unsplash

Introduction To R Shiny Modules

Mwavu's photo
Mwavu
·Mar 16, 2022·

13 min read

I've been using R for around 2 years now (today being March 14th 2022), and not just that... Almost on a daily basis. Literally. As such, I've come across a handful of blogs and youtube videos. Many of them be like "R Programming Full Course In 7 hours" and "R Programming, From Beginner To Expert In 3 hours" . Others even go to the extent of "Learn R Programming In 100 seconds"... I just be there like:

image.png

Well, let's be honest here. It ain't that easy. Not when you've never written a line of code before. The key, as you'd agree, is consistency. Practice. Day in, day out.

So even as you go through this series, remember:

image.png

Alright, pep talk's over. Let's get down to business.

Assumptions

  1. You have some good knowledge of how functions work in R.
  2. You have built one, two or three shiny apps (of any size).

Introduction

If you have those two whipped up your pocket, then let's get rolling.

image.png

In this first post of this series, I am just going to try and explain what modules are then guide you through your first R shiny module!

What Are Modules?

The first time I heard of modules I was scared. For some reason from how I was introduced to them it felt like there's R here, Shiny there, and finally modules on the other side of the river. They just felt so alien.

image.png

If I were to go back in time to explain to myself what modules are, I'd simply say this...

Modules are just like any other R function you've ever written, but... Boy oh boy! Wait for it... They are that magic wand you've been missing when creating your shiny applications.

image.png

Modules are all that a normal R function is but they're namespaced.

Whoa, whoa, whoa! Don't run away. One of the first things I learnt early in programming is that these giant names should never scare you away. Never. Usually the more complicated the name, the easier it is to understand its meaning.

What is namespacing, you ask. I learn best via examples, so here we go:

Say you're building this three-tabbed shiny app. On each tab the user needs to upload some data then calculations are performed on it. Let's assume you're uploading data about flowers (Don't we all love iris?), and the 3 datasets are completely different.

We all know coming up with variable names is one of the hardest things. On the server side of the first tab you go like:

flower_data <- ...blahblahblah...

Onto the second tab's server side:

flower_data_1 <- ...blahblahblah...

And finally the third tab:

flower_data_2 <- ...blahblahblah...

We already have a problem here.

image.png

By the time you get to the bottom of server.R you don't even know which flower is which flower. You will scroll up and down trynna confirm till the end of the world. You wanna waste your finger tip scrolling your screen just to confirm which flower is which flower? If you said yes just close this tab and continue with whatever you were doing before but I'll tell you one thing... Life's pretty short. Don't waste those 25 hours trynna memorize which inputId is which.

That's where modules come in.

image.png

What if I told you you could reuse the same inputId a gazillion times within the same shiny application? What if I told you you never have to think about coming up with creative inpuIds which sooner or later come to haunt you?

But if you could, would you really want to?

image.png

We exist in a field of infinite possibilities but everything boils down to choice. Every choice you make shuts down an infinite number of doors and opens an infinite number of doors. And now you have to make a choice...

This is your last chance. After this there is no turning back. You take the blue pill, the story ends; you wake up in your bed and believe whatever you want to believe. You take the red pill, you stay in Wonderland and I show you how deep the rabbit hole goes.

image.png

Your First Module

Setup

I always advice myself to make sure my workflow is inside an RStudio Project. RStudio is like butter to my bread, the Snoop to my Dogg.

image.png

But if you aren't using RStudio, I'm pretty sure you have your cool ways. Go ahead and open the closest thing that simulates a project.

File => New Project => New Directory => Shiny Application => Directory Name

Let's use shiny-modules as our directory name. Then choose the directory where you put your projects in. Finally, I use git for version control so I'm gonna tick "Create a git repository".

image.png

Then of course, click "Create Project".

I always like my apps to be in the ui.R, server.R structure. So delete the initialized app.R and create the following files in your project root directory:

  1. ui.R: This will be the "main" UI of the application.
  2. server.R which will be the "main" server of our application.
  3. global.R: This is where I like to load all packages used by my application. For now this file will only contain library(shiny).

We'll also need the folder /R which will contain our shiny modules. To create this you can use RStudio's GUI (Graphic User Interface) or just run this line of code on your console:

dir.create("R")

Your project structure should now look like this:

image.png

Let's Code!

I'm using R version 4.1.3

It's time to bring our previous example to life: A 3 tabbed shiny application where the user uploads "different" datasets on each tab, calculations performed and some output given.

I know it's just for demonstration purposes but uploading a dataset on each tab would be kinda dumb here.

So instead let's do this...

  1. Tab 1: (This will be our first module)

    • User uploads the data,
    • Preview is shown.
    • Button to take user to the next tab.
  2. Tab 2: (Our second module)

    • Take the data from tab 1,
    • Let user make some variable selections and
    • Plot those variables against each other.
  3. Tab 3: (Final module)

    • Take the variables selected in tab 2 and
    • Plot their distribution (histograms etc)

It'll be a small application but enough to cover major aspects of modules. In this first part of the series we'll cover the first tab.

Guess which dataset we're going to use... Taarraaannnn... iris.

image.png

Tab 1: Data Input

Currently your global.R file should only contain this one line:

library(shiny)

In our case it'd make sense to use navbarPage() over tabsetPanel()s so let's put this in our ui.R:

ui <- navbarPage(
  title = "Shiny Modules",
  id = "tab_container",

  tabPanel(
    title = "Data Input",
    value = "tab_input"
  ),

  tabPanel(
    title = "Plot",
    value = "tab_plot"
  ),

  tabPanel(
    title = "Distributions",
    value = "tab_distributions"
  )
)

Let's also populate our server.R with 'nothing':

server <- function(input, output, session) {

}

Save and click Run App (For us RStudio fans).

image.png

I don't like the look of that. Theme 'flatly' from {shinythemes} has never failed me so add this to global.R:

library(shinythemes)

... and add the theme just after the navbarPage() id:

id = "tab_container",
# <---This:--->
theme = shinytheme(theme = "flatly"),
# <--- --->

image.png

Okay, we can work with that.

Remember I stated earlier that modules are just R functions on steroids! (Did I say that though?)

A complete module has two parts... You guessed right! The ui and server parts. Which means a module is testable on it's own without the rest of the application.

How do I create a module, you ask. Let's start with the ui part.

Create a new file and put it inside the /R folder we created earlier. I always prefer to give a file the name of the function it contains, so in our case it'll be data_input_ui.R. Let's go ahead and define its contents.

Step 1: It's a function that MUST take the argument id:

data_input_ui <- function(id) {

}

Step 2: I always advice myself to wrap the contents inside shiny::tagList():

data_input_ui <- function(id) {
  shiny::tagList(

  )
}

tagList() basically takes anything that you've been doing in your application(s) ui.R.

Step 3: NS()

I don't know how much I can stress this, both to you and me. Any inputId you'll have in this ui part MUST be wrapped in NS(). This is the namespacing we were talking about earlier.

Lemme give an example. Say you have a selectInput() that allows a user to select their favorite number from 1:10.

This is how you'd do it and it is how you'll be doing it from today henceforth:

selectInput(
      inputId = NS(namespace = id, id = "your_usual_input_id"), 
      label = "Favorite Number", 
      choices = 1:10
)

It's basically passing the id that you fed into your module ui function as the namespace for the selectInput(). Don't worry, it'll soon click.

image.png

And now continuing with our agenda, the first tab should allow file upload, show preview, and have a next tab button:

data_input_ui <- function(id) {
  shiny::tagList(
    sidebarLayout(
      sidebarPanel = sidebarPanel(
        # 1. ----Upload File----
        fileInput(
          inputId = NS(namespace = id, id = "flowers"),
          label = "Choose File (.csv)",
          accept = ".csv"
        )
      ),

      mainPanel = mainPanel(
        fluidRow(
          # center contents:
          align = "center",

          column(
            width = 12,

            # 2. ----Preview----
            DT::DTOutput(
              outputId = NS(namespace = id, id = "preview")
            ) |>
              # Show loading spinner while uploading:
              shinycssloaders::withSpinner(
                type = 2,
                color.background = "white"
              ),

            # 3. ----Next Tab Btn----
            actionButton(
              inputId = NS(namespace = id, id = "go_to_plot"),
              label = "Proceed To Plot Tab",
              class = "btn-success"
            )
          )
        )
      )
    )
  )
}

See? Apart from NS() it feels just like a normal shiny application.

image.png

Question is, How do we actually call function data_input_ui()?

Answer is: Go to your ui.R, inside the tabPanel() with title Data Inputand call it inside there, of course with an id:

tabPanel(
    title = "Data Input",
    value = "tab_input",
    # <---here--->
    data_input_ui(id = "data_input")
    # <--- --->
  ),

Setting id = "data_input" is like saying:

Hey Shiny, all the code I defined in this function should be in a namespace called "data_input"

Click Run App again and see the beauty:

image.png

Babysteps!

The module is not complete yet. Let's define its server part.

Create another file inside /R folder and call it data_input_server.R. Yes, I like corresponding ui and server files to have similar names.

Step 1: Again, it's a function which must take the argument id, just like data_input_ui():

data_input_server <- function(id) {

}

Step 2: It calls another function, moduleServer():

data_input_server <- function(id) {
  moduleServer(
    id = id
  )
}

Step 3: Which calls another function:

image.png

data_input_server <- function(id) {
  moduleServer(
    id = id, 

    module = function(input, output, session) {

    }
  )
}

The innermost function is what you're used to when building your module-less shiny applications.

Basically what data_input_server() is saying is:

I MUST take an argument called id, then I will call a certain module's server with the given id. The module server will finally make a call to a normal server function.

image.png

Okay, okay. I also don't know what I'm trynna communicate here fellas.

Let's try that again:

I MUST take the argument id. To make namespacing possible I will call moduleServer() with the given id, just like you used to call NS() in your module's ui. moduleServer()will then immitate a normal server function.

That's better. If you can't wrap your mind around it just remember this and you'll be home and dry:

image.png

Step 4: Write your normal server function.

No calls to NS() like we did in our module's ui. Just reference the inputIds in the module's ui normally.

data_input_server <- function(id) {
  moduleServer(
    id = id,

    module = function(input, output, session) {
      # ----read in data----
      # I always add `r_` to all my reactive variables:
      r_flowers <- reactive({
        file <- input$flowers

        # don't proceed if user hasn't chosen a file:
        req(file)

        # read chosen csv file:
        read.csv(file$datapath)
      })

      # ----preview----
      output$preview <- DT::renderDT({
        r_flowers()
      })
    }
  )
}

Now we need to test if that's working, I mean, we garra call the function. So head into the application's main server ie. server.R and call data_input_server() with the same id as the one we gave to data_input_ui(). And now your server.R should contain this:

server <- function(input, output, session) {
  data_input_server(id = "data_input")
}

Before we Run App let's create the .csv file we'll be using. Head over to your console and run this:

dir.create(path = "data")
write.csv(x = iris, file = "data/iris.csv")

Now click Run App, choose the file we just created and see what happens.

image.png

Alright, we made it! But... Dayuumm that's gotta be the worst user experience I've ever had in my entire life boy!

  • At app startup I'm seeing some button floating anyhowly.
  • I hate clicking from one page of the rendered table to the next. I'm more of a scroll person (Like I'm really smooth, know what I'm saying?)

image.png

Let's fix those two issues RIGHT NOW!

Issue 1: Hide button at app startup.

We have two options here:

  • Use {shinyjs} to hide the button at app startup then unhide it once user uploads the data.
  • Use uiOutput() and render the actionButton() only after user uploads the data.

Either of them works, but I'll go with the {shinyjs} solution. Go to data_input_ui() and wrap the actionButton() inside shinyjs::hidden():

# Would feel nice to include a break before the action button:
tags$br(), 

# 3. ----Next Tab Btn----
shinyjs::hidden(
    actionButton(
        inputId = NS(namespace = id, id = "go_to_plot"),
        label = "Proceed To Plot Tab",
        class = "btn-success"
     )
)

Now go to data_input_server() and define an observer:

# ----unhide `go_to_plot` btn----
observe({
    # Don't unhide if user hasn't yet chosen file:
    req(r_flowers())

    # once file is chosen show btn:
    shinyjs::show(
      id = "go_to_plot"
    )
})

Finally head over to the main ui ie. ui.R and anywhere inside the first tabPanel() declare usage of {shinyjs}:

tabPanel(
    title = "Data Input",
    value = "tab_input",

    shinyjs::useShinyjs(),

    data_input_ui(id = "data_input")
  ),

Click Run App again and test if we're headed to the moon.

image.png

Issue 2: Enable scrolling of the rendered DT::datatable().

Create a new file inside /R and call it datatable_options.R. Now let's create a function to add additional functionality to our datatable(). (I told you I'm smooth)

datatable_options.R should contain:

datatable_options <- function(
    given_dataframe,
    extensions = "Scroller",
    class = "cell-border",
    scrollY = 400, # Visible table height
    scrollX = TRUE, # Enable horizontal scrolling if table is too wide
    scroller = TRUE # Use the extension "Scroller"
) {
  DT::datatable(
    given_dataframe,
    extensions = extensions,
    class = class,
    options = list(
      deferRender = TRUE,
      scrollY = scrollY,
      scrollX = scrollX,
      scroller = scroller
    )
  )
}

Now head over to data_input_server() and pass r_flowers() to the above function:

# ----preview----
output$preview <- DT::renderDT({
    r_flowers() |>
      datatable_options()
})

Time for Run App:

image.png

Tell me that ain't looking pretty and hot!

image.png

The only thing that's not done yet in this first tab is the functionality of the actionButton(): Switch to the next tab. Let's do it.

The first thing to note is that the actionButton() is inside our module data_input_ui(), which is called by tabPanel() which is in turn called by navbarPage() which is inside our main ui ie. ui.R.

navbarPage() -> tabPanel() -> data_input_ui() -> actionButton()

What that means is that if we're to call updateNavbarPage() in data_input_server(), we have to provide it with our parent_session.

How do we do that? As we agreed earlier, data_input_server() can be used like any other R function. So let's add an argument parent_session to it:

data_input_server <- function(
    id,
    parent_session
  ) {
# <--- whatever we did before --->
}

And now let's observe when user clicks our button, then switch tabs as required:

Still inside data_input_server():

# ----switch to `tab_plot`----
observeEvent(input$go_to_plot, {
  updateNavbarPage(
    session = parent_session,
    inputId = "tab_container", # id of `navbarPage()`
    selected = "tab_plot"
  )
})

Since data_input_server() has a new argument parent_session, head over to our main server function ie. server.R and provide the argument:

server <- function(input, output, session) {
  data_input_server(
    id = "data_input",
    parent_session = session
  )
}

As usual, click Run App and see the magic! Did it switch the tabs?

image.png

Done!

That's it! One tabPanel() down. Pat yourself on the back, you deserve it.

If you want to view/download/fork the whole code for this tutorial, you can find it here.

If you didn't understand anything, kindly let this be your takeaway for your own sake:

See you soon!

 
Share this