The problem:
When using bslib::navset_tab(), each tab panel is dynamically created and destroyed as you switch between them. This means that any inputs (like selectInput()) in inactive tabs are removed from the DOM. As a result, trying to update them from the server with updateSelectInput() or similar functions fails or causes infinite loops.
navset_tab(
nav_panel("Panel 1", selectInput("filter_ip1", ...)),
nav_panel("Panel 2", selectInput("filter_ip2", ...))
)
Here, only one of these selectInput() elements exists at a time. When you switch tabs, the other is destroyed and recreated later. So if you try this:
observeEvent(input$filter_ip1, {
updateSelectInput(session, "filter_ip2", selected = input$filter_ip1)
})
it won’t work — because when Panel 1 is active, filter_ip2 simply doesn’t exist.
bslib has an option called keep_alive that tells Shiny to keep all tab panels in memory instead of destroying them when hidden. This allows inputs to persist and be updated normally.
navset_tab(
id = "main_tabs",
keep_alive = TRUE,
nav_panel("Panel 1", selectInput("filter_ip1", "IP to exclude (panel 1):", choices = NULL)),
nav_panel("Panel 2", selectInput("filter_ip2", "IP to exclude (panel 2):", choices = NULL))
)
Now the synchronization works perfectly:
observeEvent(input$filter_ip1, ignoreInit = TRUE, {
updateSelectInput(session, "filter_ip2", selected = input$filter_ip1)
})
observeEvent(input$filter_ip2, ignoreInit = TRUE, {
updateSelectInput(session, "filter_ip1", selected = input$filter_ip2)
})
updateSelectInput() and observeEvent() work normally| Behavior | Without keep_alive | With keep_alive = TRUE |
|---|---|---|
| Tab switching | Destroys hidden tab | Keeps all tabs alive |
| Inputs | Recreated every time | Persistent |
updateSelectInput() | Fails or loops | Works normally |
| Use case | Lightweight UIs | Dashboards / Interactive apps |
TL;DR: If your Shiny app uses bslib::navset_tab() and you need to share state or update inputs across tabs, add keep_alive = TRUE and everything will just work.