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:
library(shiny)
library(reactives)
reactiveConsole(TRUE)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] TRUEAssigning a reactive with [<- binds it, adding the
slot or replacing its reactive, and assigning NULL with
[<- removes the slot, as for a list:
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] TRUEA 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:
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] 10A 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
[:
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