4

I'm relatively new to R Shiny and reactive programming. From my understanding (and this tutorial), it seems like you are not supposed to tell Shiny "when" to do things (i.e. to enforce execution order) as it will figure that out itself. However, often I find myself wanting plots or other UI to render sequentially. Is there a good way to do this?

I've made up a minimal example below. I want to render header before plot, as plot requires a time-consuming computation.

library(shiny)

ui <- fluidPage(
    tags$h1("My app"),
    uiOutput("header"),
    plotOutput("plot")
)

server <- function(input, output) {
            output$header <- renderUI({
                tagList(tags$h2("Section header"), 
                        tags$p("Some information relevant to the plot below..."))
            })
            
            output$plot <- renderPlot({
                # hypothetical expensive computation
                Sys.sleep(2)
            
                # hypothetical plot
                hist(rnorm(20))
            })
}

shinyApp(ui = ui, server = server)

Here I could obviously replace uiOutput("header") in ui with its definition in server and that solves the problem; however, in practice I want header to be dynamic. A hacky solution I found was to include a hidden input inside header and then use req() inside plot. This is kind of like adding an action button that automatically clicks upon load.

server <- function(input, output) {
            output$header <- renderUI({
                tagList(tags$h2("Section header"), 
                        tags$p("Some information relevant to the plot below..."),
                        div(style = "display:none", textInput(inputId = "hidden", label = "", value = "x")))
            })
            
            output$plot <- renderPlot({
                req(input$hidden)
                
                ...
            })
}

However, if I want to repeat this in multiple situations or if I want to force a chain of more than two outputs to render sequentially, then this seems tedious. Can anyone suggest a more elegant solution?

antsatsui
  • 151
  • 1
  • 4
  • But if you run the code above you'll see that they appear at the same time, which is not what I want. To make it more clear that the page is loading, you can add text above `uiOutput("header")`. You'll notice that the header stays blank until the plot is ready. – antsatsui Jan 22 '21 at 04:33
  • 1
    Good point -- I edited my post to use Sys.sleep. – antsatsui Jan 22 '21 at 04:57
  • `renderPlot` and `renderUI` dont have a `priority` argument, but `observeEvent` does. Perhaps if you make you plot and header each depend on reactives generated by [`observeEvent`](https://shiny.rstudio.com/reference/shiny/1.0.3/observeEvent.html)s with different priorities, that will give you what you want. – Limey Jan 22 '21 at 12:48
  • 1
    I tried doing this: I made a reactiveValues to store the header and plot data, then used two `observe` calls to modify the reactiveValues, and then called the reactiveValues from within each `render`. Unfortunately playing around with priority did not change anything -- the header always executed first, but waited for the plot to finish before displaying. Another method I tried was just wrapping `output$header <- ...` (and same for plot) within an observe statement -- then `priority` let me change execution order, but I still had the issue of them waiting for each other to load. – antsatsui Jan 22 '21 at 18:07

2 Answers2

2

As your example code includes a "time-consuming computation" I guess in the end you don't really want to control the render order, instead you want to avoid long running functions to block the execution of other parts of your code (XY problem).

By default, R is single threaded, therefore we'll need child processes to solve this issue. In you can use library(future) + library (promises) for this. However, using asynchronous processes to unblock the elements within a shiny session requires us to "hide" the promise - read more about it here.

Below please find an async version of your example:

library(shiny)
library(promises)
library(future)
plan(multisession)

ui <- fluidPage(
  tags$h1("My app"),
  uiOutput("header"),
  plotOutput("plot")
)

server <- function(input, output) {
  output$header <- renderUI({
    tagList(tags$h2("Section header"), 
            tags$p("Some information relevant to the plot below..."))
  })
  
  data <- reactiveVal()
  
  observe({
    future_promise({
      # hypothetical expensive computation
      Sys.sleep(2)
      # hypothetical plot data
      rnorm(20)
    }, seed=TRUE) %...>% data()
    return(NULL) # "hide" the future_promise
  })
  
  output$plot <- renderPlot({
    req(data(), cancelOutput = TRUE)
    hist(data())
  })
}

shinyApp(ui = ui, server = server)

result

ismirsehregal
  • 30,045
  • 5
  • 31
  • 78
1

In Shiny, for a given session, no outputs are sent back to the client, until all outputs are ready : the text render function isn't sent until the plot render function completes, see shiny flush cycle.
A workaround is to skip plot rendering using a reactiveVal so that the text gets displayed in a first flush cycle, and then to use invalidateLater() to launch a new flush cycle to run the longer plot rendering.


library(shiny)

ui <- fluidPage(
  tags$h1("My app"),
  uiOutput("header"),
  plotOutput("plot")
)

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

  skipPlot <- reactiveVal(1)

  output$header <- renderUI({
    tagList(tags$h2("Section header"),
            tags$p("Some information relevant to the plot below..."))
  })

  output$plot <- renderPlot({
    
    if (isolate(skipPlot()==1)) {
      # skip first reactive sequence
      skipPlot(0)
      # launch next reactive sequence
      invalidateLater(1,session)
    } else {
      # hypothetical expensive computation
      Sys.sleep(2)

      # hypothetical plot
      hist(rnorm(20))

    }

  })
}

shinyApp(ui = ui, server = server)

enter image description here

Waldi
  • 39,242
  • 6
  • 30
  • 78