Author: Krystian Igras
Despite their advantages, Dynamic Shiny Modules can destabilize the Shiny environment and cause its reactive graph to be rendered multiple times. In this blog post, I present how to remove deleted module leftovers and make sure that your Shiny graph observers are rendered just once.
While working with advanced Shiny applications, you have most likely encountered the need for using Shiny Modules. Shiny Modules allow you to modularize the code, reuse it to create multiple components using single functions and prevent the code’s duplication.
Perhaps the best feature of Shiny Modules is the ability to create dynamic app elements. A great implementation of this can be found here. This particular example provides convenient logic for adding and removing variables and their values in a reactive manner.
Implementing Shiny Modules does come with certain challenges that can affect the stability of your Shiny environment. In this article, I will show you how to overcome them.
Removing the remnants of an obsolete module
Removing a module can have a destabilizing impact on your Shiny app environment. To illustrate this problem, let’s consider this simple application:
The app allows the user to create (and remove) a new module that counts the number of clicks of the button placed inside of the module. The number of clicks is also displayed outside the module in order to see the internal module value after that module is removed.
The expectation is that removing the module would remove its internal objects including input values. Unfortunately, this is not the case:
In fact, removing the module only affects the UI part while the module’s reactive values are still in the Shiny session environment and are rewritten right after a new module is called. This becomes particularly problematic when the module stores large inputs. Adding a new module aggregates the memory used by the application and can quickly exhaust all the available RAM on the server hosting your application. This issue can be resolved by introducing the remove_shiny_inputs
function, as explained here. The function allows you to remove input values from an unused Shiny module.
In our implementation, making use of the function requires a simple modification of the remove_module
event:
observeEvent(input$remove_module, {
removeUI(selector = "#module_content")
shinyjs::disable("remove_module")
shinyjs::enable("add_module")
remove_shiny_inputs("my_module", input)
local_clicks(input[["my_module-local_counter"]])
})
Removing internal observers that have registered multiple times
The second issue has likely contributed to hair being ripping out of many Shiny programmers’ heads.
Observe events, just like reactive values in the example above, are not removed when a module is deleted. In this case, the issue is even more serious – the obsolete observer is replicated rather than overwritten. As a result, the observer is triggered as many times as the new module (with the same id) was created.
In our example, adding a simple print function inside an observeEvent
shows the essence of this issue:
observeEvent(input$local_counter, {
print(paste("Clicked", input$local_counter))
local_clicks(input$local_counter)
}, ignoreNULL = FALSE, ignoreInit = TRUE
)
This behavior may cause your application to slow down significantly within just a few minutes of use.
The fastest solution to this problem is a workaround which requires the developer to create new modules with unique identifiers. This way, each new module creates a unique observer and the previous observers are not triggered anymore.
The proper solution, not as commonly known, is offered directly by the Shiny package and does not require any hacky workarounds. We begin by assigning the observer to the selected variable:
my_observer <- observeEvent(...)
We have two options to apply this solution to our example.The my_observer
object now allows us to use multiple, helpful methods related to the created observer. One of them, destroy()
, provides for correctly removing a Shiny observer from its environment, with:
The first approach calls for assigning the module’s observer to a variable that is accessible from within the Shiny server directly. For instance, we can use reactiveVal
that is passed to the module and designed to store observers.
The second approach makes use of the session$userData object
(see Marcin Dubel’s related blog post).
We decided to use the second approach, so we assigned an observer to the session$userData$my_observer
variable:
session$userData$clicks_observer <- observeEvent(...)
Then, we modified the remove_module
event by adding a destroy action on the variable we just created:
observeEvent(input$remove_module, {
removeUI(selector = "#module_content")
shinyjs::disable("remove_module")
shinyjs::enable("add_module")
remove_shiny_inputs("my_module", input)
local_clicks(input[["my_module-local_counter"]])
session$userData$clicks_observer$destroy()
})
The result met our expectations:
The final application code is available here.
Conclusion
Shiny offers great functionalities for creating advanced, interactive applications.
Whilst this powerful package is easy to use, we still need to properly manage the application’s low-level objects to ensure optimal performance. If you have any examples of how you struggled with solving other less common Shiny challenges, please share in the comment section below!