Skills · R
A store every module can reach with no arguments, so a value picked in one module is read reactively in another without a chain of parameters through every level.
Source the file, create the store once in the main server with its initial values, and in each module call shared_state(session) instead of threading reactives through arguments.
Needs: shiny. License: MIT. Written from the method the session teaches; the instructor's own materials come with the recording.
The No-Look Pass. The skill is the method from that session, packaged so you can use it before you have watched it; the session is where the reasoning lives. Every skill in the library names its session the same way.
shared_state.R · 86 lines
# shared_state.R
#
# Shared state between Shiny modules without passing a chain of arguments through every level:
# the pattern from the AthlyticZ session "The No-Look Pass" (Veerle Eeftink-van Leemput), as one
# small file you can source() into any app.
#
# source("shared_state.R")
#
# server <- function(input, output, session) {
# store <- shared_state(session, team = NULL, season = 2026) # create once, at the top
# pickerServer("pick") # writes store$team
# headToHeadServer("h2h") # reads store$team
# }
#
# pickerServer <- function(id) moduleServer(id, function(input, output, session) {
# store <- shared_state(session) # fetch it anywhere, no arguments
# observeEvent(input$team, store$set("team", input$team))
# })
#
# headToHeadServer <- function(id) moduleServer(id, function(input, output, session) {
# store <- shared_state(session)
# output$title <- renderText(paste("Head to head:", store$get("team"))) # reactive read
# })
#
# How it works, in two sentences. The session object is one shared object for the whole app, and
# session$userData is a plain environment on it that every module can reach without being handed
# anything; the store keeps a reactiveValues() there, so a read inside a reactive context
# re-runs when the value changes, and the top-level and module sessions resolve to the same
# root session so they see the same store. Nothing is global, two apps in one process never
# share state, and testServer() works because each test gets its own session.
#
# What it does not do. It is not namespaced: a key is one key for the whole app, so name keys
# for what they are ("team", "season"), not for the module that writes them. If two modules
# write the same key you have a design problem, not a bug in this file.
#
# License: MIT. Written for athlyticz.com/skills from the method the session teaches; the
# session's own app and code come with the recording.
library(shiny)
shared_state <- function(session = shiny::getDefaultReactiveDomain(), ...) {
stopifnot(!is.null(session))
root <- session
while (!is.null(root$rootScope) && !identical(root$rootScope(), root)) root <- root$rootScope()
if (is.null(root$userData$.shared_state)) {
init <- list(...)
root$userData$.shared_state <- do.call(shiny::reactiveValues, init)
} else if (length(list(...))) {
warning("shared_state() already exists on this session; initial values ignored", call. = FALSE)
}
rv <- root$userData$.shared_state
structure(list(
get = function(key) rv[[key]], # reactive read: call inside a reactive context
set = function(key, value) { rv[[key]] <- value; invisible(value) },
peek = function(key) shiny::isolate(rv[[key]]), # non-reactive read, for event handlers
keys = function() shiny::isolate(names(shiny::reactiveValuesToList(rv))),
rv = rv # the reactiveValues itself, if you want it
), class = "shared_state")
}
print.shared_state <- function(x, ...) {
cat("<shared_state>", paste(x$keys(), collapse = ", "), "\n")
invisible(x)
}
# A minimal runnable example. Run with: shiny::runApp(shared_state_demo())
shared_state_demo <- function() {
pickerUI <- function(id) selectInput(NS(id, "team"), "Team", c("Phoenix", "Seattle", "Las Vegas"))
pickerServer <- function(id) moduleServer(id, function(input, output, session) {
store <- shared_state(session)
observeEvent(input$team, store$set("team", input$team), ignoreInit = FALSE)
})
readerUI <- function(id) h3(textOutput(NS(id, "title")))
readerServer <- function(id) moduleServer(id, function(input, output, session) {
store <- shared_state(session)
output$title <- renderText(paste("Selected without any arguments:", store$get("team")))
})
shinyApp(
ui = fluidPage(pickerUI("pick"), readerUI("read")),
server = function(input, output, session) {
shared_state(session, team = "Phoenix")
pickerServer("pick")
readerServer("read")
}
)
}
First name and email. One message when a session recording, a skill, a cheat sheet or a posting worth your time arrives here, and no other mail. Replies reach a person.