Skip to contents

An app often holds a set of reactives that changes while it runs, such as one for each module the user adds. The set needs keys, so that one of its reactives can be read, replaced or removed without disturbing the others. The shiny package offers reactiveValues() for keyed stored values, but it can neither remove nor reorder keys, and it has no keyed collection of computed values. A reactives collection holds reactives of both kinds in slots that can be added, removed and reordered, and it tracks each slot as a dependency of its own.

At the console, reading a reactive fails unless the read is wrapped in isolate(). The examples below turn on shiny’s reactive console, which lifts that requirement, so that they read as they would inside an app:

Slots hold reactives

A collection holds reactives, such as a reactiveVal() for a stored value or a reactive() for a computed one, each in a slot of its own. It has two levels, as a list does: the slots, and the values their reactives return. Reading with $ or [[ returns a slot’s value, as on a reactiveValues() object, while reading with [ returns the slot’s reactive:

a <- reactiveVal(1)
x <- reactives(a = a, b = reactive(2 * 21))

x$a
#> [1] 1
x[["b"]]
#> [1] 42
identical(x["a"], a)
#> [1] TRUE

Assigning a reactive with [<- binds it, adding the slot or replacing its reactive, and assigning NULL with [<- removes the slot, as for a list:

x["c"] <- reactive(x$a + 1)
x["b"] <- NULL
names(x)
#> [1] "a" "c"

Assigning with $<- or [[<- writes a value instead, as on a reactiveValues() object. Where the slot holds a reactiveVal(), the value is written through it, so the slot keeps its reactive and code holding that reactiveVal() sees the new value. Where there is no slot, a new reactiveVal() holding the value is bound to it:

x$a <- 10
a()
#> [1] 10
x$c
#> [1] 11

x$d <- "new"
x$d
#> [1] "new"

Unlike on a list, assigning NULL with $<- or [[<- stores NULL rather than removing the slot, which only [<- does. A slot that stores NULL reads as NULL with $, as a missing slot does, but [ tells the two apart, since the slot still has a reactive:

x$d <- NULL
names(x)
#> [1] "a" "c" "d"
is.null(x$d)
#> [1] TRUE
is.null(x["d"])
#> [1] FALSE
is.null(x["e"])
#> [1] TRUE

A slot holding a computed reactive can’t be written, so that a stray value can’t overwrite it. Replacing its reactive takes [<-:

x$c <- 5
#> Error:
#> ! A slot holding a reactive expression can't be written. Bind a `reactiveVal()` to the slot with `[<-` to replace the expression.

For a collection of stored values, reactive_vals() wraps each value it is given in a reactiveVal(). Such a collection only ever holds reactiveVal() slots, so each of its slots can be written, and code moving to it from a reactiveValues() object keeps its reads and writes with $ and [[. The as_values() function returns the value of every slot, as reactiveValuesToList() does for reactiveValues():

y <- reactive_vals(count = 1, label = "one")
y$count <- 2
as_values(y)
#> $count
#> [1] 2
#> 
#> $label
#> [1] "one"

For a collection of computed values, reactive_exprs() is the counterpart. It only accepts reactive expressions, such as those that reactive(), eventReactive() or debounce() return, so none of its slots can be written. Assigning a value with $<- or [[<- is an error even where there is no slot, which keeps a stray value from adding one:

w <- reactive_exprs(double = reactive(2 * y$count))
w$double
#> [1] 4

w$triple <- 6
#> Error:
#> ! A value can't be written to a `reactive_exprs` collection, which only holds reactive expressions. Bind a reactive expression to the slot with `[<-` instead.
names(w)
#> [1] "double"

A collection is shared

A collection is shared, as a reactiveValues() object is: after z <- y, both names refer to the same collection, and a change made through either is seen by everything holding it. That is what lets one part of an app change a collection while another part reacts to it. It is also why reorder() changes the collection it is given, rather than returning a reordered copy:

z <- y
reorder(z, c("label", "count"))
names(y)
#> [1] "label" "count"

Each slot is a dependency of its own

Reading a slot makes the reader depend on that slot alone: the reader re-runs when the slot is bound, replaced or removed, but not when another slot changes. Reading the slot’s value with $ or [[ also makes the reader depend on the value, as calling the slot’s reactive does. A reactive() that reports each computation shows this, since it only recomputes once something it read has changed:

x <- reactive_vals(a = 1, b = 2)

a_doubled <- reactive(
  {
    message("Computing a_doubled")
    2 * x$a
  }
)

a_doubled()
#> Computing a_doubled
#> [1] 2

x$b <- 3
x$c <- 4
reorder(x, c("c", "b", "a"))
a_doubled()
#> [1] 2

x$a <- 5
a_doubled()
#> Computing a_doubled
#> [1] 10

A reader of a slot that doesn’t exist yet gets NULL, and it re-runs once the slot is added. Once a slot is removed, its readers re-run and get NULL in turn.

Testing whether a slot exists by looking for its name in names(x) would re-run the reader whenever a slot is added, removed or moved. The has_key() function tests for slots with the same dependencies as reading their reactives with [:

has_key(x, c("a", "d"))
#> [1]  TRUE FALSE

Other reads depend on what they return. The length() method depends on the number of slots alone, names() on which slots exist and their order, and as_values() on all of that and on every slot and its value. A reader that lists the slots, for example to offer them as choices, re-runs when a slot is added, removed or moved, but not when a value changes:

slot_names <- reactive(
  {
    message("Reading the names")
    names(x)
  }
)

slot_names()
#> Reading the names
#> [1] "c" "b" "a"

x$a <- 6
slot_names()
#> [1] "c" "b" "a"

x["b"] <- NULL
slot_names()
#> Reading the names
#> [1] "c" "a"

See the Dependencies section of ?reactives for reads by position.

In an app

The app below lets the user add terms and remove them again, and it shows their total. Each term is a module, and each module binds a reactive returning its term’s value to the collection that the app passes it. Since a collection is shared, the app sees every slot a module binds or removes. The terms are all computed, so the app keeps them in a reactive_exprs() collection, where a stray write such as terms$extra <- 1 fails rather than adding a term to the total.

term_ui <- function(id) {

  ns <- NS(id)

  div(
    id = ns("term"),
    numericInput(ns("value"), id, value = 1),
    actionButton(ns("remove"), "Remove")
  )
}

term_server <- function(id, terms) {
  moduleServer(
    id,
    function(input, output, session) {

      terms[id] <- reactive(input$value)
      session$onDestroy(function() terms[id] <- NULL)

      observeEvent(
        input$remove,
        {
          removeUI(paste0("#", session$ns("term")))
          session$destroy()
        }
      )
    }
  )
}

ui <- fluidPage(
  actionButton("add", "Add a term"),
  div(id = "terms"),
  textOutput("total")
)

server <- function(input, output, session) {

  terms <- reactive_exprs()

  observeEvent(
    input$add,
    {
      id <- paste0("term_", input$add)
      insertUI("#terms", ui = term_ui(id))
      term_server(id, terms)
    }
  )

  output$total <- renderText(
    paste("Total:", sum(unlist(as_values(terms))))
  )
}
shinyApp(ui, server)

The total reads every slot’s value, so it re-runs whenever a term is added, removed or changed, while a reader of names(terms) would re-run only when a term is added or removed.

A collection belongs to the session or module it was created in, here the app’s session, and is destroyed along with it. Changing it from a module doesn’t tie it to that module. A reactive bound to a slot, however, belongs to the module that created it and is destroyed with that module, after which calling it fails. That is why each module removes its slot in session$onDestroy(), which runs however the module is destroyed, so that no reader of the collection calls a destroyed reactive. By contrast, in a collection that can be written, the reactiveVal() that $<- or [[<- creates for a new slot belongs to the collection, whichever module writes the value.

Testing

Inside testServer(), the test code sees the server’s collection and outputs, so an app like the one above can be checked without a browser. The collection belongs to the session that testServer() creates, though, and can’t be read once the test is over. To check it afterwards, take a snapshot with snapshot_reactives() while the session is still open. A snapshot belongs to no session, and each of its slots returns the value the original slot had when the snapshot was taken:

snap <- NULL

testServer(
  server,
  {
    session$setInputs(add = 1)
    session$setInputs(add = 2)
    session$setInputs(`term_1-value` = 3, `term_2-value` = 4)
    session$setInputs(`term_1-remove` = 1)
    print(output$total)
    snap <<- snapshot_reactives(terms)
  }
)
#> [1] "Total: 4"

as_values(snap)
#> $term_2
#> [1] 4