thiagogps
thiagogps

Reputation: 489

Shiny only calculating when the user is looking at the output

I'm having a problem with Shiny and I don't know if it's a bug or I'm missing something. I tried to find a solution but I can't seem to find references to this problem online.

I have a function that is supposed to run when I press a button, but it's only running when the user is looking at the output after the button is pressed.

It's somewhat weird, so I created a minimal example. I have two tabs. In the first, I choose a number (mean), and in the second tab I plot values around that mean. The button is in the first tab, but the "create.plot" function only starts to run when you go to tab2. To make things clear, I created this 'loading.div', which shows up when the function is running.

library(shiny)
library(shinyjs)

create.plot <- function(mean)
{
  shinyjs::show('loading.div')
  y <- rep(NA,10)
  for(i in 1:10)
  {
    y[i] <- rnorm(1,mean=mean)
    Sys.sleep(0.5)
  }
  shinyjs::hide('loading.div')

  return(list(y=y, test='Now it runs from tab1'))
}

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

  run.function <- eventReactive(input$run,create.plot(input$mean))

  output$plot.y <- renderPlot({
    plot(run.function()[['y']])
  })

  output$test <- renderText({
    run.function()[['test']]
  })
}

ui <- fluidPage(shinyjs::useShinyjs(),
  hidden(div(id='loading.div',h3('Loading..'))),
  tabsetPanel(

  tabPanel('tab1',
    numericInput(inputId = 'mean',label='Mean', value=1),
    actionButton(inputId = 'run', label='Run')
    #,textOutput('test')   ## Uncommenting this line, it works.
    ),

  tabPanel('tab2',
    plotOutput('plot.y'))
  )
)

shinyApp(server=server,ui=ui)

To make sure that the problem was that the user was not looking at the output when at tab1, I created a test string. If you uncomment that line, the problem is "fixed": the function runs directly after the user hits the button. However, that only works because he is looking at an output in tab1.

This is becoming a serious problem because I have a shiny dashboard with many tabs. There are tabs with inputs and tabs with outputs. I'd like the function to run when I click a button, but it's only running when the user goes to the "outputs" tab.

Is this a known problem? Are there workarounds?

Upvotes: 9

Views: 4515

Answers (2)

Benjamin
Benjamin

Reputation: 17279

Reactive elements that are not visible are considered "suspended" and will not update their values until they are unsuspended. Be default, this doesn't happen until they are rendered on the screen.

You can set an element to update even when not visible by changing its suspended status. You do this with the outputOptions function.

for the output$test element, you would need to run

outputOptions(output, "test", suspendWhenHidden = FALSE)

If you have multiple elements you want to update even when not visible, you can use

lapply(c("test1", "test2", "test3"),
       function(x) outputOptions(output, x, suspendWhenHidden = FALSE)

Be aware, however, that this means that all of the calculations will be performed at once. Depending on the size of your calculations, this could slow down the app performance. For instance, if there is a plot that is generated and takes some time to render, and only a small portion of users will ever look at the plot, it might be prudent to leave it suspended until a user requests it to prevent slowing down the calculation of more commonly used elements.

EDITS

It turns out "plots cannot render in advance because they have no width and height. " (https://github.com/rstudio/shiny/issues/1409). So we have to do some trickery to make the plot render in advance:

We can choose the dimensions of our plot instead of letting shiny fit it automatically. This will mean that the plot will not grow if the screen is enlarged.

library(shiny)
library(shinyjs)

create.plot <- function(mean)
{
  print("working.....")
  shinyjs::show('loading.div')
  y <- rep(NA,10)
  for(i in 1:10)
  {
    y[i] <- rnorm(1,mean=mean)
    Sys.sleep(0.5)
  }
  shinyjs::hide('loading.div')

  y
}

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

  run.function <- eventReactive(input$run,create.plot(input$mean))

  output$plot.y <- renderPlot({
      plot(run.function())
    },
    width = 72 * 5,
    height = 72 * 3
  )

  outputOptions(output, "plot.y", suspendWhenHidden = FALSE)
}

ui <- fluidPage(shinyjs::useShinyjs(),
                hidden(div(id='loading.div',h3('Loading..'))),
                tabsetPanel(

                  tabPanel('tab1',
                           numericInput(inputId = 'mean',label='Mean', value=1),
                           actionButton(inputId = 'run', label='Run')
                  ),

                  tabPanel('tab2',
                           plotOutput('plot.y'))
                )
)

shinyApp(server=server,ui=ui)

Upvotes: 12

Mike Wise
Mike Wise

Reputation: 22847

Actuall that is the way it is supposed to work. That is what being "reactive" is all about - it does lazy evaluation with caching (so you do not have to). If you want to force something to be calculated, put it in a reactiveValue and calculate it with observeEvent (this does eager execution), and then display the values with reactive blocks.

I modified your example to do that here:

library(shiny)
library(shinyjs)

create.plot <- function(mean) {
  shinyjs::show('loading.div')
  y <- rep(NA,10)
  for (i in 1:10) {
    y[i] <- rnorm(1,mean = mean)
    Sys.sleep(0.5)
  }
  shinyjs::hide('loading.div')

  return(list(y = y,test = 'Now it runs from tab1'))
}

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

  rv <- reactiveValues(plotvals=NULL)

  observeEvent(input$run,{rv$plotvals <- create.plot(input$mean) })

  output$plot.y <- renderPlot({
    req(input$run)
    plot(rv$plotvals[['y']])
  })

  output$test <- renderText({
    rv$plotvals[['test']]
  })
}

ui <- fluidPage(shinyjs::useShinyjs(),
  hidden(div(id = 'loading.div',h3('Loading..'))),
  tabsetPanel(

  tabPanel('tab1',
    numericInput(inputId = 'mean',label = 'Mean',value = 1),
    actionButton(inputId = 'run',label = 'Run')
#,textOutput('test')   ## Uncommenting this line, it works.
    ),

  tabPanel('tab2',
    plotOutput('plot.y'))
  )
)

shinyApp(server = server,ui = ui)

Yielding this (right after pressing the run button):

enter image description here

And then when that text goes away, you can see the plot in tab2 immediately.

enter image description here

To better understand this stuff, and the lazy nature of reactives and the eager nature of observe, see Joe Cheng's presentation on it here. It is well worth the time:

https://www.rstudio.com/resources/webinars/shiny-developer-conference/

In fact the reactive nature of Shiny is exactly what makes it so productive, once you get used to it - it is rather different.

Upvotes: 5

Related Questions