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
removeUI(selector = "#module_content")
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:
}, 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$clicks_observer <- observeEvent(...)
Then, we modified the
remove_module event by adding a destroy action on the variable we just created:
removeUI(selector = "#module_content")
The result met our expectations:
The final application code is available here.
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!