shiny icon indicating copy to clipboard operation
shiny copied to clipboard

Obtuse error on mutually dependent modules with stopifnot() checks

Open asadow opened this issue 1 year ago • 1 comments

Do you think there's a way to improve the error on mutually dependent module servers that use stopifnot() on their inputs? The current error of "object [return name of module server] not found" has confused me more than once.

library(shiny)

moduleA_Server <- function(id, squared) {
  ## A then B in server() causes object 'bmod' not found
  stopifnot(is.reactive(squared))
  
  moduleServer(id, function(input, output, session) {
    observe({
      print(squared())
      })
    list(input_num = reactive(input$input_num))
  })
}

moduleA_UI <- function(id) {
  ns <- NS(id)
  tagList(
    sliderInput(ns("input_num"), "Choose a number", 
                min = 1, max = 10, value = 5),
    textOutput(ns("moduleA_output"))
  )
}

moduleB_Server <- function(id, input_num) {
  ## B then A in server() causes object 'amod' not found
  # stopifnot(is.reactive(input_num))
  
  moduleServer(id, function(input, output, session) {
    squared <- reactive({
      input_num()^2
    })
    
    output$moduleB_output <- renderText({
      paste("Square of number:", squared())
    })
    
    list(squared = squared)
  })
}

moduleB_UI <- function(id) {
  ns <- NS(id)
  tagList(
    textOutput(ns("moduleB_output"))
  )
}

ui <- fluidPage(
  titlePanel("Two-Module App with Interdependency"),
  sidebarLayout(
    sidebarPanel(
      h3("Module A"),
      moduleA_UI("moduleA"),
      h3("Module B"),
      moduleB_UI("moduleB")
    ),
    mainPanel()
  )
)

server <- function(input, output, session) {
  ## A then B
  amod <- moduleA_Server("moduleA", squared = bmod$squared)
  bmod <- moduleB_Server("moduleB", input_num = amod$input_num)
  
  ## B then A
  # bmod <- moduleB_Server("moduleB", input_num = amod$input_num)
  # amod <- moduleA_Server("moduleA", squared = bmod$squared)
}

shinyApp(ui = ui, server = server)

asadow avatar Oct 03 '24 19:10 asadow

One approach that works – and that I can't say for sure is The Best Approach – is to use reactiveValue()s that exist outside of the modules and that carry the values from the other modules. This lets you ensure that the reactive exist in the global Shiny app, independently of the two submodules.

server <- function(input, output, session) {
  input_num <- reactiveVal()
  squared <- reactiveVal()

  ## A then B
  # amod <- moduleA_Server("moduleA", squared = squared)
  # bmod <- moduleB_Server("moduleB", input_num = input_num)


  ## B then A
  bmod <- moduleB_Server("moduleB", input_num = input_num)
  amod <- moduleA_Server("moduleA", squared = squared)

  observe(input_num(amod$input_num()))
  observe(squared(bmod$squared()))
}

gadenbuie avatar Oct 03 '24 20:10 gadenbuie

I love this approach for 3 reasons:

  1. It allows me to note and define, in one spot (e.g. at the beginning of a server function as you have done), which of the app's reactives are sent between modules
  2. The order of the module server object assignments in the main server() does not matter
  3. It makes it possible to stopifnot(is.reactive()) on all reactives within the module server functions

asadow avatar Jan 02 '25 16:01 asadow