Changing scenes

library(scenes)

When a {shiny} app loads, it receives a request object, with properties such as QUERY_STRING (the ?x=1&y=2 part of a url) and HTTP_COOKIE (the names and values of cookies). Use {scenes} to switch between alternative {shiny} UIs depending on properties of that request object.

Why?

It’s possible to process the request using a UI function, instead of a standard shiny::tagList(). So why not process the request through such a function?

I created {scenes} to write a single login process for the apps I produce for the R4DS Online Learning Community. The goal was to create each UI without having to think about login, and then wrap those UIs in the common login framework. That process became the {shinyslack} package.

Perhaps you have your own login process. Or perhaps you want to show completely different UIs to different customer segments visiting the same URL, depending on a cookie or a query parameter. {scenes} exists to enables these workflows.

A toy example

Here I’ll demonstrate a simple example of changing UIs based on various request parameters. You can see a deployed version of this app at https://r4dscommunity.shinyapps.io/scenes/.

The UIs

First we’ll create four simple UIs. You can ignore the specifics of these UIs for now, but you might want to come back to see how they work once you run the app. In a real {scenes} app, these should each be a shiny::tagList() UI or UI function.

# ui1 loads if none of the requirements are met.
ui1 <- shiny::tagList(
  shiny::p("This is UI 1."),
  shiny::a("Add '?code' to the URL to see UI 2.", href = "?code")
)

# ui2 allows us to create the cookie requirement for ui3.
ui2 <- cookies::add_cookie_handlers(
  shiny::tagList(
    shiny::p("This is UI 2."),
    shiny::actionButton("cookie_simple", "Store Simple Cookie"),
    shiny::p("Press the button to see UI 3.")
  )
)

# ui3 allows us to update that cookie to one that will pass validation.
ui3 <- cookies::add_cookie_handlers(
  shiny::tagList(
    shiny::p("This is UI 3."),
    shiny::actionButton("cookie_valid", "Store Valid Cookie"),
    shiny::p("Press the button to see UI 4.")
  )
)

# ui4 only loads when everything is all set. It has a button to reset things.
ui4 <- cookies::add_cookie_handlers(
  shiny::tagList(
    shiny::p("This is UI 4."),
    shiny::actionButton("reset", "Reset"),
    shiny::p("Press the button to go back to UI 2.")
  )
)

We’ll use actions to decide which of those UIs to display.

Scenes and actions

In {scenes}, a shiny_scene associates a UI with one or more scene_actions that are used to choose it. In this case, we’ll display our UIs in these four situations:

  • Display ui4 when the user has a particular cookie set and the value of that cookie successfully passes a validation function.
  • Display ui3 when the user has that cookie set, but their value doesn’t validate.
  • Display ui2 when the user has a particular parameter in the URL query string.
  • Display ui1 when none of those cases are true. In a real app, this final UI would likely be the login screen, or perhaps an error page.

In this toy example, our cookies are “valid” if they have a certain value. That value changes sometimes, so we create a validation function that accepts both the cookie value and the acceptable value.

our_cookie_validator <- function(cookie_value, acceptable) {
  cookie_value == acceptable
}

We wrap ui4 with the req_has_cookie() action, into a shiny_scene.

scene4 <- set_scene(
  ui4,
  req_has_cookie(
    cookie_name = "our_cookie",
    validation_fn = our_cookie_validator,
    acceptable = "good value" # We can pass variables through to our validator.
  )
)

The shiny_scene for ui3 is similar, but we skip the validation. In other words, they must have the cookie set, but we don’t care what value it has.

scene3 <- set_scene(
  ui3,
  req_has_cookie(
    cookie_name = "our_cookie"
  )
)

For ui2, we’re looking for a parameter named “code”. We don’t care what the value is (if we did, we’d pass a vector of acceptable values).

scene2 <- set_scene(
  ui2,
  req_has_query("code")
)

Finally, we set up a scene without any actions for our fall-through UI.

scene1 <- set_scene(
  ui1
)

Scene changes

We wrap our scenes together with change_scene(). We list the scenes in priority order.

ui <- change_scene(
  scene4,
  scene3,
  scene2,
  scene1
)

We can use this ui just like any other {shiny} UI.

# Any UI that the user sees will use this
# shared server backend.
server <- function(input, output, session) {
  # If they press the button in ui2, save a cookie and reload.
  shiny::observeEvent(
    input$cookie_simple,
    {
      cookies::set_cookie("our_cookie", "bad value")
      session$reload()
    }
  )
  
  # If they press the button in ui3, save a "valid" cookie and reload.
  shiny::observeEvent(
    input$cookie_valid,
    {
      cookies::set_cookie("our_cookie", "good value")
      session$reload()
    }
  )
  
  # If they press the reset button in ui4, delete the cookie and reload.
  shiny::observeEvent(
    input$reset,
    {
      cookies::remove_cookie("our_cookie")
      session$reload()
    }
  )
}

shiny::shinyApp(
  ui = ui,
  server = server
)