Best way to pass action button input from one module to another

I am working on a larger shiny app and I would like to include a module that works as a menu. In this module, you can choose between other standalone modules and activate one with an action button click. The chosen module will pop up as a shiny modal.

However, I cannot find a good way to pass the action button input from one module to another, because if I pass it as a reactive value it will only work once but not the second time after closing the module. I believe this is because the observeEvent does not record any change in the passed reactive value.

Do you have any suggestions on how to fix this problem? Do I need a totally different approach?

Here is a working example:

# Load packages
library(shiny)

# mod_1 ui
# This modules task is to pass the button press to mod_3 in order to activate a modal there.
mod_1_ui <- function(id){

  ns <- NS(id)
  tagList(
    actionButton(ns("actionbtn_1"), label = "actionbtn_1")
  )
}

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

    fruit <- reactiveVal(value = NULL)

    observeEvent(input$actionbtn_1, {
      fruit("banana")
    })

    return(list(
      activate_1 = reactive({fruit()})
      ))
}

# mod_2 has no ui
# This modules' task is to recieve the action button press from mod_1 and activate the modal if it is pressed.
mod_2 <- function(input, output, session, activate_1, activate_2){

  modal <- function() {
    ns <- session$ns

    modalDialog(
      footer = actionButton(ns("close_modal"), label = "Close modal"),
      textOutput(ns("fruit"))
    )
  }

  observeEvent(activate_1(), {
    if(activate_1() == "banana") {
      showModal(modal())}   

    })

  observeEvent(input$close_modal, {

    removeModal()
  })

  output$fruit <- renderText({
    paste("first mod:", activate_1())
  })

}

# app ui
app_ui <- 
    fluidPage(
      fluidRow(
      mod_1_ui("mod_1")
      )
    )

# app server
app_server <- function(input, output,session) {
  rv <- callModule(mod_1, "mod_1")
  callModule(mod_2, "mod_2", activate_1 = rv$activate_1)
}

# running the app
shinyApp(app_ui, app_server)

ps.: the question is posted on StackOverflow as well.

If you're going to ask a question in multiple places please cross-link them.

You might want to read more about how to use modules (and pass data between them) at https://mastering-shiny.org/scaling-modules.html

Thank you I will add the cross-link.

I've read this chapter before but I will give it another look. In the meantime I feel that the problem is not passing the reactive data between the modules but resetting the reactive value after it is used for updating the modal. As far as I know the action button increases an integer every time it is called and thus the observeEvent is triggered. But in my case I want to pass an identifier that opens the specific model from the menu which remains constant after passing.

Maybe I am looking at the problem from a wrong direction. Can you please tell me what am I missing?

If I was going to look at your code, I'd first re-write into the style now recommended by that chapter. I don't have the time to do that, but if you do, I'm happy to have another look.

Since reactives are lazy, you may easily see that in your code, activate_1() never changes value since fruit is always equal to banana. You must reset it or change it so that activate_1 fires and opens the modal more than once. Therefore, I added a feedback from module 2 to module 1. This notifies module 1 when the modal was closed. Listening to this feedback, I reset the fruit reactive value in module 1. This way, clicking on the actionButton will show the modal again and again:

library(shiny)

# mod_1 ui
# This modules task is to pass the button press to mod_3 in order to activate a modal there.
mod_1_ui <- function(id) {
  ns <- NS(id)
  tagList(
    actionButton(ns("actionbtn_1"), label = "actionbtn_1")
  )
}

# mod_1 server
mod_1 <- function(input, output, session, feedback) {
  fruit <- reactiveVal(NULL)

  observeEvent(feedback(), {
    fruit(NULL)
  })

  observeEvent(input$actionbtn_1, {
    fruit("banana")
  })

  return(list(
    activate_1 = reactive({
      fruit()
    })
  ))
}

# mod_2 has no ui
# This modules' task is to recieve the action button press from mod_1 and activate the modal if it is pressed.
mod_2 <- function(input, output, session, activate_1, activate_2) {
  ns <- session$ns
  modal <- function() {
    modalDialog(
      footer = actionButton(ns("close_modal"), label = "Close modal"),
      textOutput(ns("fruit"))
    )
  }

  observeEvent(activate_1(), {
    if (activate_1() == "banana") {
      showModal(modal())
    }
  })

  observeEvent(input$close_modal, {
    removeModal()
  })

  output$fruit <- renderText({
    paste("first mod:", activate_1())
  })

  return(reactive(input$close_modal))
}

# app ui
app_ui <-
  fluidPage(
    fluidRow(
      mod_1_ui("mod_1")
    )
  )

# app server
app_server <- function(input, output, session) {
  rv <- callModule(mod_1, "mod_1", feedback)
  feedback <- callModule(mod_2, "mod_2", activate_1 = rv$activate_1)
}

# running the app
shinyApp(app_ui, app_server)

Hope I am answering your question

2 Likes

Thank you for your response! This is what I was looking for.

Thank you for the offering, I will still review the code based on the style outlined in the chapter.

This code makes me feel a bit worried:

rv <- callModule(mod_1, "mod_1", feedback) 
feedback <- callModule(mod_2, "mod_2", activate_1 = rv$activate_1)

Because you have a circular dependency. I think that's going to be hard to reason about

1 Like

Here's what I came up with — the returned value of an action button already increments each time you push it, so I don't think you need any additional gymnastics. I also switched to modern module style, and gave the modules informative names:

library(shiny)
moduleServer <- function(id, module) {
  callModule(module, id)
}

button_ui <- function(id) {
  actionButton(NS(id, "btn"), label = "Go")
}

button_server <- function(id) {
  moduleServer(id, function(input, output, session) {
    reactive(input$btn)
  })
}

module_server <- function(id, button) {
  moduleServer(id, function(input, output, session) {
    modal <- modalDialog(
      "Let's start a dialog",
      footer = actionButton(NS(id, "close_modal"), label = "Close"),
      textOutput(NS(id, "fruit"))
    )
    observeEvent(input$close_modal, {
      removeModal()
    })
    observeEvent(button(), {
      showModal(modal)
    })
  })
}

ui <- fluidPage(
  fluidRow(
    button_ui("mod1")
  )
)
server <- function(input, output, session) {
  button <- button_server("mod1")
  module_server("mod_2", button)
}
shinyApp(ui, server)
2 Likes

Thank you! You are right, using the incremental value of the action button as the trigger is the most simple solution, and it lets you avoid the possibility of a loop as well.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.