3

How to display a blank UI (alternatively destroy module UI), if the module server-function fails, without moving all the UI-code to the server function?

Simple reproducible example:

library(shiny)

my_module_ui <- function(id) {
  ns <- NS(id)
  tags$div(
    tags$h1("Don't show me if my_module_server fails!"),
    plotOutput(ns("my_plot"))
  )
}

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

  tryCatch({
    my_data <- cars * "A" # fail for demo
    # my_data <- cars

    output$my_plot <- renderPlot({
      cars2 <- my_data + rnorm(nrow(my_data))
      plot(cars2)
    })
  }, error=function(cond) {
    message("Destroy UI here!")
  })


}

ui <- fluidPage(
  my_module_ui("my_id")
)

server <- function(input, output, session) {
  callModule(my_module_server, "my_id")
}

shinyApp(ui, server)

My current solution is to have nothing but a uiOutput() in my_module_ui and render the entire ui in the server function. I want to prevent this, since large modules get very messy if all UI-components are placed within the module server-function.

In addition I would preferably also like to avoid returning values from callModule() that destroy the UI and do this from within the server-function instead.

Thanks!

Comfort Eagle
  • 2,112
  • 2
  • 22
  • 44
  • oh, i didnt read the lower part of your question carefully before posting my answer. That could be in conflict with my answer. May i ask why you `preferably also like to avoid returning values from callModule()`? – Tonio Liebrand Nov 21 '19 at 00:21
  • Thanks anyway:) The infrastructure of my app doesn't allow it for reasons that might require too much explanation for a comment. My modules feed into `session`, inspired by the following blog post: https://appsilon.com/super-solutions-for-shiny-architecture-1-of-5-using-session-data/?nabe=4634331497365504:1&utm_referrer=https%3A%2F%2Ft.co%2FzeGsGOgyWE%3Famp%3D1 – Comfort Eagle Nov 21 '19 at 00:29
  • ok, partly understood. How about you leverage the session object as well for the purpose? See my edit. If i understood you correctly that would a) not render ui fully on server side and b) not return a value from the `callModule()`. – Tonio Liebrand Nov 21 '19 at 11:42

2 Answers2

2

How about you assign a value to the session object and evaluate this value before you create the UI (from server side via renderUI().

1) Move rendering of UI to server side

Use renderUI(my_module_ui("my_id")) on server side and uiOutput("module") on ui side.

2) To detect whether your server module was successful assign a value to the session object

my_module_server <- function(input, output, session) {
  tryCatch({
     ...
    session$userData$mod_server <- TRUE
  }, error = function(cond) {
    session$userData$mod_server <- NULL
  })
}

3) Use this value to make the call of your module ui conditional

  output$module <- renderUI({
    callModule(my_module_server, "my_id")
    if(!is.null(session$userData$mod_server)) my_module_ui("my_id")
  })

Reproducible example:

library(shiny)

my_module_ui <- function(id) {
  ns <- NS(id)
  tags$div(
    tags$h1("Don't show me if my_module_server fails!"),
    plotOutput(ns("my_plot"))
  )
}

my_module_server <- function(input, output, session) {
  tryCatch({
    my_data <- cars * "A" # fail for demo
    # my_data <- cars

    output$my_plot <- renderPlot({
      cars2 <- my_data + rnorm(nrow(my_data))
      plot(cars2)
    })
    session$userData$mod_server <- TRUE
  }, error = function(cond) {
    session$userData$mod_server <- NULL
  })
}

ui <- fluidPage(
  uiOutput("module")
)

server <- function(input, output, session) {
  output$module <- renderUI({
    callModule(my_module_server, "my_id")
    if(!is.null(session$userData$mod_server)) my_module_ui("my_id")
  })
}
shinyApp(ui, server)
Tonio Liebrand
  • 17,189
  • 4
  • 39
  • 59
  • 1
    Really nice use of `session$userData`, didn't know it could be used like that. I will have to refactor some of my apps after this discovery. This is so much better than my answer. – MalditoBarbudo Nov 22 '19 at 09:55
1

With a little code reordering, and the use of the amazing shinyjs package this can be done.

Note that I added an input to simulate errors and not errors, to see how the UI dissapears. Also all is done in the server part of the module. I hope this will help you. The code has inline comments explaining the steps.

library(shiny)
library(shinyjs)

my_module_ui <- function(id) {
  ns <- NS(id)

  tagList(
    # input added to be able to throw errors and see the ui dissapear
    selectInput(
      ns('trigger'), 'Error trigger',
      choices = list('no error' = c(2,1), 'error' = c('A', 'B')),
      selected = 2
    ),
    tags$div(
      # div with id, to select it with shinyjs and hide it if necessary
      id = ns('hideable_div'),
      tags$h1("Don't show me if my_module_server fails!"),
      plotOutput(ns("my_plot"))
    )
  )
}

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

  # get all the things prone to error in a reactive call, that way you capture the final
  # result or a NULL reactive when an error occurs
  foo <- reactive({

    tryCatch({

      if (input$trigger %in% c(2,1)) {
        trigger <- as.numeric(input$trigger)
      } else {
        trigger <- input$trigger
      }

      cars * trigger
    }, error=function(cond) {
      message("Destroy UI here!")
    })
  })

  # obseveEvent based on the error reactive, to check if hide or not the UI
  observeEvent(foo(), {
    # hide checking if foo is null, using shinyjs
    if (is.null(foo())) {
      shinyjs::hide('hideable_div')
    } else {
      shinyjs::show('hideable_div')
    }
  }, ignoreNULL = FALSE, ignoreInit = FALSE)


  # outputs, with validation of the error reactive. That way code after validate is not
  # executed but the app does not get blocked (gray)
  output$my_plot <- renderPlot({
    shiny::validate(
      shiny::need(foo(), 'no data')
    )
    cars2 <- foo() + rnorm(nrow(foo()))
    plot(cars2)
  })

}

ui <- fluidPage(
  # really important for shinyjs tu work!!!!!!!
  shinyjs::useShinyjs(),
  my_module_ui("my_id")
)

server <- function(input, output, session) {
  callModule(my_module_server, "my_id")
}

shinyApp(ui, server)

MalditoBarbudo
  • 1,815
  • 12
  • 18
  • Thanks, that's definitively a solution worth considering! :) I would prefer preventing the initial rendering (if possible) but mark your answer as accepted if no other solutions to accomplish this come along! Thanks again – Comfort Eagle Nov 19 '19 at 11:03
  • @Comfort Eagle Don't worry about marking the answer if this don't fulfill your expectations ;) The problem I see is that if you want to maintain the ui code in the UI, not in the server, I think there is no way to avoid rendering without JS or shinyjs. But I hope people with more expertise than me prove me wrong ;) – MalditoBarbudo Nov 19 '19 at 14:14