Super Solutions for Shiny Apps #1: Using Session Data
The session argument can help you organize the app’s content. In this article I explain how to use it as a global list for passing parameters between modules in advanced Shiny apps to simplify how objects flow in the code. Managing dependencies between modules with sessions this way is much faster than doing it manually.
This is the first in a series of tips and tricks for advanced Shiny developers. The ideas presented in the series will help keep your Shiny apps bug-free, transparent, organized, fast, and reliable.
The potential issue of “passing values between modularized applications” does not necessarily come up when writing simple “Hello World” applications. However, most advanced Shiny practitioners eventually run into this problem when creating multi-view, modularized applications. The official RStudio article “Communication between modules” offers one solution that works reasonably well – returning the list of all inputs from one module and using them as parameters in subsequent module calls. It does come with certain limitations, so let us consider these downsides first and discuss an alternative solution.
Context & the Problem
Shiny was first designed for simple prototype applications used by data scientists to help in their everyday work. The community quickly discovered, however, that Shiny’s potential is far greater and started to develop advanced dashboards.
This can create significant problems – keeping the entire app structure in just two files, ui and server, makes the development process far less effective as the code base grows in size. Further, once you build a large chunk of the application you might wish to re-use it in your other projects. While Shiny modules do provide for that – developers can now create parts of the code in separate files and combine them into advanced dashboards – there is a tradeoff here. On the one hand, the separate parts need to be interdependent within the app. On the other hand, they have to be independent enough to facilitate their use elsewhere.
Balancing this can be very difficult to achieve as an app grows in size and its modules’ dependencies become more complicated. It eventually gets difficult to organize all the traffic between the saved results and parameters passed to modules.
Let’s explore this issue on a real life example of nested modules: there is a screen (1st tier module) with a few tables (a single table is a 2nd tier module) and each table consists of a few columns (a column is a 3rd tier module). There is also a module with filters (e.g. opened on modal) with sections for each table and for a single condition.
The basic Shiny system would require passing the parameters up-and-down the dependency ladder. Imagine that you need to use yet another parameter from single-condition in single-column. Such a simple operation might require quite a lot of changes in parameters passed in each module. While this should work just fine, especially if organized properly, the session solution can be far more efficient and less cumbersome for the developer. Let us dive into the details.
As you probably know, the ui part of the module should always contain at least the id parameter and the server part always contains the boilerplate parameters input, output, session (for more information, this Joe Cheng post provides an introduction to Shiny modules). Session is not required by default but it can be used to save user data. It has to be added as an argument to the main server function as well as to each of the modules.
Since each module has access to the session argument, anything that we store there will be universally accessible. Indeed, you can consider it a global list of stored values. You may be familiar with ns <- session$ns, which is the same concept. Now we will use it for passing the other values as well.
Let’s explore this idea by modifying the example presented by RStudio.
In this rather simple app the module scatterplot_server_mod requires three additional parameters (dataset, plot1vars, plot2vars) to be passed to the module call. We will try to simplify it and keep the functionality working without using any external parameters. It might not be strictly necessary in this case, but as we saw in our example above, things can escalate quickly.
All we need to do is save the results from the server file to the list stored in the global session rather than to local objects. Let’s turn this:
We can now use the values from the session argument within the module as we see fit. To simplify things further, it can be assigned to the same variables and the rest of the code will remain the same:
One of the key advantages of this solution is that we can call our module in different places, for instance nested in another module on a separate modal screen, without having to consider whether the variables will be available for it there or if we need to recall them. They are always there waiting for us in the session argument!
Please note: while one of the advantages of Shiny modules is that they can be easily transferred to other applications, reusing objects passed through session violates module independence – there is code inside the module that uses external objects without stating them explicitly as server arguments. Although session technical is one such parameter, it is not clear what objects it is required to contain – session is a huge list object, and we are interested in the contents of its sublist userData. Therefore, it is important to keep your code organized.. First, each module should contain detailed documentation. Second, the names of session’s objects should be reassigned to simpler names at the beginning of the module to avoid missing any dependencies.
To sum up: keeping the results of the modules in a session object will help you organize the app content and simplify the objects flow logic. It is faster than managing all of the dependencies between modules. We recommend this approach for advanced Shiny apps.