R Shiny applications are served as a single page application and it is not built to render multiple pages. There are benefits of rendering multiple pages such as code management and implement authentication. In this page, we discuss how to implement multi-page rendering in a Shiny app.

As indicated above, Shiny is not designed to render multiple pages and, in general, the UI is rendered on the fly as defined in ui.R or app.R. However this is not the only way as the UI can be rendered as a html output using htmlOutput() in ui.R and renderUI() in server.R. In this post, rendering multiple pages will be illustrated using an example application.

Example application structure

A total of 6 pages exist in the application as shown below.

At the beginning, the login page is rendered. A user can enter credentials for authentication or move to the register page. User credentials are kept in a SQLite db and the following user information is initialized at each start-up - passwords are encrypted using the bcrypt package.

1library(bcrypt)
2app_name <- "multipage demo"
3added_ts <- format(Sys.time(), "%Y-%m-%d %H:%M:%S")
4users <- data.frame(name = c("admin", "john.doe", "jane.doe"),
5                    password = unlist(lapply(c("admin", "john.doe", "jane.doe"), hashpw)),
6                    app_name = c("all", rep(app_name, 2)),
7                    added_ts = rep(added_ts, 3),
8                    stringsAsFactors = FALSE)
9users
1##       name                                                     password
2## 1    admin $2a$12$RhUwtbJnr3Uo75npOeE96u1eRGpyQD2tJ38S2lCJ7wtBa.THxMGf2
3## 2 john.doe $2a$12$Svzr/4/Ti5u6YVgx04Cy7OXhar71NgjD.gPpoX3hUJ4Pgd.gN1V.u
4## 3 jane.doe $2a$12$CGAfSfYWP9eOuZxM1njwtOfGR2MlqDbcCeUE.CkXlZvBGHPlSORDW
5##         app_name            added_ts
6## 1            all 2016-06-10 17:59:39
7## 2 multipage demo 2016-06-10 17:59:39
8## 3 multipage demo 2016-06-10 17:59:39

Note that the authentication plan of this application is for demonstration only. In practice, for instance, LDAP or Active Directory authentication may be considered if it is possible to contact to a directory server - for Active Directory authentication, the radhelper package might be useful.

It is assumed that an application key (application-key) should be specified for registration together with user name and password. The register page is shown below.

Once logged on, two extra buttons appear: Profile and App. The screenshots of before and after login are shown below.

The main purpose of the profile page is to change the password.

The application page keeps the main contents of the application. The default Shiny application is used.

UI elements

Each UI elements are constructed in a function and it is set to be rendered using htmlOutput() in ui.R. Actual rendering is made by renderUI() in server.R.

Below shows the main login page after removing CSS and Javascript tags.

 1ui_login <- function(...) {
 2  args <- list(...)
 3  fluidRow(
 4    column(3, offset = 4,
 5           wellPanel(
 6             div(id = "login_link",
 7                 actionButton("login_leave", "Leave", icon = icon("close"), width = "100px")
 8             ),
 9             br(),
10             br(),
11             h4("LOGIN"),
12             textInput("login_username", "User name"),
13             div(class = "input_msg", textOutput("login_username_msg")),
14             passwordInput("login_password", "Password"),
15             div(class = "input_msg", textOutput("login_password_msg")),
16             actionButton("login_login", "Log in", icon = icon("sign-in"), width = "100px"),
17             actionButton("login_register", "Register", icon = icon("user-plus"), width = "100px"),
18             br(),
19             div(class = "input_fail", textOutput("login_fail")),
20             uiOutput("login_more")
21           )
22    )
23  )
24}

Each UI function has unspecified argument (...) so that some values can be passed from server.R. For example, the logout and application pages include message and username from the server.

At the end, UI is set to be rendered as a html output.

1ui <- (htmlOutput("page"))

Application logic

A page is rendered using render_page() in server.R. This function accepts a UI element function in ui.R and renders a fluid page with some extra values. I didn’t have much luck with Shiny Dashboard that the flud page layout is chosen instead.

 1render_page <- function(..., f, title = app_name, theme = shinytheme("cerulean")) {
 2  page <- f(...)
 3  renderUI({
 4    fluidPage(page, title = title, theme = theme)
 5  })
 6}
 7
 8server <- function(input, output, session) {
 9  ...
10  
11  ## render default login page
12  output$page <- render_page(f = ui_login)
13  
14  ...
15}

The authentication process shows a tricky part of implementing this setup. Depending on which page is currently rendered, only a part of inputs exist in the current page. In this circumstance, if an input is captured in reactive context such as observe() and reactive() but it doesn’t exist in the current page, an error will be thrown. Therefore whether an input exists or not should be checked as seen in the observer below. On the other hand, observeEvent() is free from this error as it works only if the input exists.

 1  user_info <- reactiveValues(is_logged = is_logged)
 2  
 3  # whether an input element exists should be checked
 4  observe({
 5    if(!is.null(input$login_login)) {
 6      username <- input$login_username
 7      password <- input$login_password
 8      
 9      if(username != "") output$login_username_msg <- renderText("")
10      if(password != "") output$login_password_msg <- renderText("")
11    }
12  })
13  
14  observeEvent(input$login_login, {
15    username <- isolate(input$login_username)
16    password <- isolate(input$login_password)
17    
18    if(username == "") output$login_username_msg <- renderText("Please enter user name")
19    if(password == "") output$login_password_msg <- renderText("Please enter password")
20    
21    if(!any(username == "", password == "")) {
22      is_valid_credentials <- check_login_credentials(username = username, password = password, app_name = app_name)
23      if(is_valid_credentials) {
24        user_info$is_logged <- TRUE
25        user_info$username <- username
26        
27        output$login_fail <- renderText("")
28        
29        log_session(username = username, is_in = 1, app_name = app_name)
30      } else {
31        output$login_fail <- renderText("Login failed, try again or contact admin")
32      }
33    }
34  })

A try-catch block can also be useful to prevent this type of error due to a missing element. Below the plot of the application page is handled in tryCatch so that the application doesn’t stop abruptly with an error although the plot element doesn’t exist in the current page.

 1tryCatch({
 2  output$distPlot <- renderPlot({
 3    # generate bins based on input$bins from ui.R
 4    x    <- faithful[, 2]
 5    bins <- seq(min(x), max(x), length.out = input$bins + 1)
 6    
 7    # draw the histogram with the specified number of bins
 8    hist(x, breaks = bins, col = 'darkgray', border = 'white')
 9  })
10})

I hope this post is useful.