Communication between modules and its whims

As part of the development of a Shiny application for production using {golem}, we recommend, among other things, working with Shiny-modules. The communication of data between the different modules can be complex. At ThinkR we use a strategy: the stratégie du petit r. We explain everything in this article.

A module is the combination of two functions. These functions contain input parameters. As for a function that we could define as pure, what happens inside the module is made to stay there and is not supposed to be in the global environment, except the last object created if it is exported.
As with a classic function, a parameter not declared in the function, but which exists in the global environment, can be used in it.

a <- 5
my_sum <- function(b) {
b + a
}
my_sum(b = 3)

## [1] 8

But this is not a reproducible way of working. The function is not independent of the code in which it exists.
If you follow ThinkR news, you know that to develop our shiny applications we use package {golem}. {golem} allows development in the form of modules. This makes it easier to maintain the application over time and this encourages group work.
As we work with {golem}, we create an R package. By analogy with functions, modules must be able to be considered as independent, documentable and testable with unit tests. This ensures the maintainability of the package. Thus, one could consider that a module could be re-used as is in another Shiny application. This is not always the case, but it is what we recommend to keep in mind when developing modules.

How to implement the “stratégie du petit r”

The “stratégie du petit r” allows modules to communicate using a global reactiveValues that passes through each module to retrieve the different results that we want to share between modules.

An example of a Shiny application

Let’s take a reproducible example of an independent shiny application that allows you to select a column from a selected dataset. The selected columns are displayed in a table.

Transform into a module

For the example, we will pass in module only the part “selection of a column and visualization of its content”. Selection of the dataset is done outside the module. Transforming into a module is realised using [{golem}] (//github.com/ThinkR-open/golem).
We will not detail all the steps to use {golem}, you will find more information here and there. We will simply transform the previous application into a module.
First step, create a golem for our application:

The “stratégie du petit r” from the main “server” file

Before coding, it is necessary to ensure the availability of data for our future module. In our example, to fully understand the “stratégie du petit r”, we consider the selection of the main dataset outside the column selection module.
To do this, we will create a “reactiveValues” that will retrieve the user’s choice.
Side “support_ui” for the moment (equivalent to ui.R):

You must use the ns function around the “inputId” and “outputId” to make them unique.

The server side of our module

The server side must consider the data available in the global environment and be responsive to any changes in this data.
To do this, the reactiveValues named r is shared with our module. For the documentation of the “server” function, we add a r parameter to our module.

Our mod_select_view_view_server function now takes a r parameter which will be the “reactiveValues” between modules. Within the Shiny application, you can use the content of r$dataset, i.e. the dataset chosen by the user.

Calling the module in the general app

At the general UI level (“app_ui”), it is necessary to add the function mod_select_view_ui.

All this for this? Is that all? We already know that. Your article is useless, Cervan. we need crunchy!
Stay with us reader, the crunchy part is coming!!!
We now have an app where the modules can communicate with each other. I will now introduce a problem related to reactiveValues, refreshing lists in “reactiveValues”.

The whims of reactiveValues

In the previous example, we detail how to use the “stratégie du petit r”. That’s when things start to get interesting.
Here is a piece of code that has caused some problems in applications:

If you click once on “pierre” and only then on “henri”, you will see that the message “Normalement, je clique sur pierre!” is present for each click on “henri” in your console.
In other words, the reactivity of a list in a `reactiveValues’ is propagated if one of the items in this list changes.
So you can imagine the impact this can have on communication between modules and the possibility of having code restarted several times.
With this piece of code, it’s even more obvious:

In the first example, the results of our buttons were stored in r$test so we had a list in a reactiveValues and that’s the problem. In the second example, we stored directly the buttons at the root of the reactiveValues, r$pierre and r$henri. We could then not make a sub-list in a reactiveValues. But this is not covenient. If we have several modules that store results in this reactiveValues, we won’t be able to remember where they come from. Not really easy for debugging.
How to correct this problem which could have significant impacts on communication between modules?

The “stratégie du petit r”, yes, but with nested reactiveValues

We’ll have to use the “nested” “reactiveValues”! Uh, is that coming out of your hat? Yes. A little bit. But it’s not really complicated.

Since for each first element of a reactiveValues the problem does not arise, we will have :

r <- reactiveValues(test = reactiveValues())

In this case, the problem is solved and clicking on “henri” does not trigger “pierre”!
Example:

Voila. That’s it. A big name, for a small modification that changes everything !

The advantages and disadvantages of the strategy

We have explored other possibilities for communication between modules, but this is often at the expense of code readability.

(+) An advantage of this strategy is that the data is not copied several times in the RAM.

(+) The tree structure between modules is easy to identify et we can easily find where this or that variable comes from.

(-) A disadvantage is that our module now depends on this r$dataset. If you want to reuse it in another application, you will need a r$dataset. Even if, at ThinkR, we develop Shiny modules as if they should be independent, the reality is that reusable modules represent only a small part of the developed modules. This constraint is therefore not really important. In addition, if the module were to be reused in another application, the associated documentation must indicate this constraint, in the same way that a traditional function forces the user to use a data or parameter format, or to choose input parameters.

To go further

Want to know more about {golem} and our recommendations around the development of shiny applications: