Remove / Destroy dynamically created controls to avoid memory leaks

Dear community,
I have a shiny app in which I am creating some controls dynamically (tabs, checkboxes and plots).
I am looking for some advice on How to remove the dynamically created controls.

I have tried using removeUI, which indeed removes the controls on the user interface. However when looking at what happens server-side the dynamically created controls are still present (I am using the shiny.reactlog).

What strategy should I use to remove these unused controls and avoid potential memory leaks?

I have included a sample application below. Some parts could have been written using shiny_modules but I did not want to add an extra layer of complexity.

To reproduce these observations:

  • start rstudio
  • options(shiny.reactlog=TRUE)
  • start the app (code below)
  • Click on Add controls (controls are created)
  • Click on Remove controls (controls are removed)
  • Open the react log (press Ctrl + F3)
  • Replay the script using Right-arrow key

You will notice that despite using removeUI no nodes are removed from the graph.

Than you in advance for your help.

library(shiny)
library(shinydashboard)
library(ggplot2)

# Define UI for miles per gallon app ----
ui <- pageWithSidebar(
  
  # App title ----
  headerPanel("Adding/Removing controls dynamically"),
  
  # Sidebar panel for inputs ----
  sidebarPanel(
               sliderInput(inputId = "n_controls", value = 2, min = 0, max = 5, label = "controls to add"),
               actionButton("addcontrols", "Add controls"),
               actionButton("removecontrols", "Remove Controls")
               ),
  
  # Main panel for displaying outputs ----
  mainPanel(
    uiOutput("all_controls")
  )
)

# Define server logic to plot various variables against mpg ----
server <- function(input, output) {
  
  observeEvent(input$addcontrols, {
    output$all_controls <- renderUI({create_tabset(
      isolate(input$n_controls), input, output)
    })  
  })
  
  observeEvent(input$removecontrols, {
    #remove tabset
    removeUI(".tabbable", immediate = TRUE)
  })  
  
  
}

ns_prefix<- function(type, id) {
  paste("dynamic", type, id, sep = "_")
} 

create_tabset <- function(number_controls, input, output) {
 
  force(number_controls)
  
  # create a list of tabs
  myTabs <-  lapply(1:number_controls, create_tab, input = input, output = output)
  myTabs <- unname(myTabs)
  # create a tabset containing the tabs
  out <- do.call(tabsetPanel, c(myTabs, list(id = "alltabs")))
  
  out
}

create_tab <- function(number, input, output) {
  # create controls 
  controls <- create_controls(number)
  
  # create tabpanel with embedded controls
  tab <- tabPanel(title = sprintf("Title%s", number),
            value = number,
            controls)
  
  # Make the checkbox in the tab reactive
  input_check <- reactive({input[[ns_prefix("check", number)]]})
  
  # Bind the checkbox event and the plot
  output[[ns_prefix("plot", number)]] <- renderPlot({create_plot(is_red = input_check())})

  tab
}

create_controls <- function(number) {
  # create list of controls
  # using ns_prefix to assign a specific prefix
  out <-  tagList(
                div(
                    checkboxInput(inputId = ns_prefix("check", number), label = sprintf("Checkbox%s", number)),
                    plotOutput(outputId = ns_prefix("plot", number))
                )
              )
}

create_plot <- function(df = iris, is_red) {
  # choose color
  col <- if (is_red) "red" else "black"
  
  # make plot
  out <- ggplot(iris, aes(Sepal.Length, Sepal.Width)) + 
          geom_point(color = col)
  out
}
  
shinyApp(ui, server)