Tips for module development?

recommendations

#1

I'm working on a multi-tab shiny app. I am writing each tab as a module to encapsulate the UI and server logic. I would like to be able to develop the modules independently of the main app; for example to create the UI of a module and check its results without having to navigate to it in the main app.

What I am doing is making a simple Shiny app that displays just a single module. I put the app in a function in the same file as the module. Calling the function then runs the module. Here is a simple example using the slider module from https://github.com/aoles/modules-tutorial:

library(shiny)

sliderUI = function(id) {
  ns = NS(id)
  tagList(
    sliderInput(ns('slider'), 'Slide me', 0, 100, 1),
    textOutput(ns('number'))
  )
}

slider = function(input, output, session) {
  output$number = renderText({ input$slider })
}

testSlider = function() {
  ui = fluidPage('Test', sliderUI('test'))
  server = function(input, output, session) {
    callModule(slider, 'test')
  }
  
  shinyApp(ui, server)
}

testSlider()

This works but it seems awkward and it will be more complex for modules with multiple inputs or outputs. Has anyone found a better way to do this? How do you develop modules?

Thank you!
Kent


#2

Complex "module per tab" setup:

  • a folder for each module containing >= 3 files
  • UI file, server file, engine file
  • optional test / file that creates the config / app to check the module
    • it's not a shinytest at this point; it is just to visualize the output and get quick feedback
  • other files: helper.R, constants.R

The most difficult part is the interface between the modules or with the main app: how is the data from this module sent to (and received by) other modules.

Also, depending on the app, a module can contain only the UI side or the server side (it is not required to include both).


#3

@MikeBadescu Thanks! It sounds like your test file is similar to what I included with the module in my example. What is in the engine file?


#4

I like to construct my apps so that they can run without shiny. In the engine file of each module I put stand alone functions that I call from the server file. For example, given the inputs the code to create a ggplot does not live inside the server file but in the engine file. Thus, I can source the engine files from different modules, provide inputs for their functions and check the calculations from an R script / markdown file, without calling shiny.


#5

This is a great question. Some resources and thoughts:

  • Creating standalone helper functions / "engine functions" in separate R files are certainly a great trick. One great way to use these throughout your app is to wrap them up in a package (R's favorite way to share functions), add unit tests with testthat, and load that package when necessary.
  • Creating standalone modules, as you mentioned. These can then be put into packages or files, and tested / developed independently using testthat or shinytest.
  • following the practices of those that have gone before you
  • reading more about Shiny modules

Ultimately, I am hopeful of a function like the following eventually making its way into the shiny package for easier use. The idea is to run a module independently of its parent application. I'm not 100% satisfied with the API, yet, but it's a start. Feel free to use it!

runModule <- function(
  id, 
  ui, 
  server, 
  ui_param = list(), 
  server_param = list()
  ){
  
  actualUI <- do.call(ui, c(id = id, ui_param))
  
  actualServer <- function(input, output, session) {
    do.call(callModule,
            c(module = server
              , id = id
              , server_param
              )
            )
  }
  
  shinyApp(actualUI, actualServer)
}

Call it with something like:

runModule("my_module", ui = sliderUI, server = slider)

I recommend an approach like this one to the testSlider function that you wrote, because it follows functional programming best practices that make it easier to maintain / reason about. All UI / server parameters would be passed to the ui_params and server_params variables. The only difficulty at present is if you wanted to pass a reactive value, that would be difficult in this context.