15  Reactive building blocks

Table of content for chapter 15

Chapter section list

15.1 Reactive values

There are two types of reactive values:

  • A single reactive value, created by reactiveVal().
  • A list of reactive values, created by reactiveValues().

They have slightly different interfaces for getting and setting values.

R Code 15.1 : Interfaces for getting and setting reactive values

Listing / Output 15.1: reactiveVal() and reactiveValues() have different interfaces
Code
x <- reactiveVal(10)
x()       # get
x(20)     # set
x()       # get

r <- reactiveValues(x = 10)
r$x       # get
r$x <- 20 # set
r$x       # get
#> [1] 10
#> [1] 20
#> [1] 10
#> [1] 20

While they look different, they behave the same, so you can choose between them based on which syntax you prefer. In this book Hadley use reactiveValues() because the syntax is easier to understand at a glance, but in his own code h tends to use reactiveVal() because the syntax makes it’s clear that something weird is going on.

It’s important to note that both types of reactive values have so called reference semantics. Most R objects have copy-on-modify semantics which means that if you assign the same value to two names, the connection is broken as soon as you modify one.

R Code 15.2 : Copy on modify semantics in R

Listing / Output 15.2: Copy on modify semantics in R
Code
a1 <- a2 <- 10
a2 <- 20
a1 # unchanged
#> [1] 10

Reactive values do not have copy-on-modify semantics. They always keep a reference back to the same value so that modifying any copy modifies all values:

R Code 15.3 : Reference semantics of reactive values

Listing / Output 15.3: Reactive values have reference semantics
Code
b1 <- b2 <- reactiveValues(x = 10)
glue::glue("Start: b1 and b2 have the same value. b1$x = ", {b1$x}, ", b2$x = ", {b2$x})
b1$x <- 20
glue::glue("Modification: Only b1$x has changed to ", {b1$x}, ", but b2$x has also changed to", {b2$x})
#> Start: b1 and b2 have the same value. b1$x = 10, b2$x = 10
#> Modification: Only b1$x has changed to 20, but b2$x has also changed to20

We’ll come back to why you might create your own reactive values in . Otherwise, most of the reactive values you’ll encounter will come from the input argument to the server function. These are a little different to the reactiveValues() that you create yourself because they’re read-only: you can’t modify the values because Shiny automatically updates them based on user actions in the browser.

15.1.1 Exercises

15.1.1.1 Two list of reactive values

What are the differences between these two lists of reactive values? Compare the syntax for getting and setting individual reactive values.

Exercise 15.1 : Exercise: Two lists of reactive values

Listing / Output 15.4: Exercise: Two lists of reactive values
Code
l1 <- reactiveValues(a = 1, b = 2)
l2 <- list(a = reactiveVal(1), b = reactiveVal(2))

glue::glue("l1$a = {l1$a}")
glue::glue("l1$b = {l1$b}")
glue::glue("l2$a() = {l2$a()}")
glue::glue("l2$b() = {l2$b()}")
#> l1$a = 1
#> l1$b = 2
#> l2$a() = 1
#> l2$b() = 2

Content of l1 are values, content of l2 are functions

15.1.1.2 Has reactiveVal() reference semantics?

Design and perform a small experiment to verify that reactiveVal() also has reference semantics.

Exercise 15.2 : Exercise: Has reactiveVal() reference semantics?

Listing / Output 15.5: Exercise: Has reactiveVal() reference semantics?
Code
r1 <- r2 <- reactiveVal(10)
r1(20)
r2()
#> [1] 20

Yes, reactiveVal() also has reference semantics: I initialized r1 & r2 with reactiveVal(10), then I changed only to value of r1(20)- Printing the content of r2() results also in 20, although I hadn’t change r2()directly.

15.2 Reactive expressions

Recall that a reactive has two important properties: it’s lazy and cached. This means that it only does work when it’s actually needed, and if called twice in a row, it returns the previous value.

There are two important details that we haven’t yet covered: what reactive expressions do with errors, and why base::on.exit() works inside of them.

15.2.1 Errors

Reactive expressions cache errors in exactly the same way that they cache values. For example, take this reactive:

R Code 15.4 : Reactive expressions cache errors

Listing / Output 15.6: Reactive expressions cache errors
Code
r <- reactive(stop("Error occured at ", Sys.time(), call. = FALSE))
r()
#> Error: Error occured at 2025-07-17 10:49:09.82687
Code
Sys.sleep(2)
r()
#> Error: Error occured at 2025-07-17 10:49:09.82687

Errors are also treated the same way as values when it comes to the reactive graph: errors propagate through the reactive graph exactly the same way as regular values. The only difference is what happens when an error hits an output or observer:

  • An error in an output will be displayed in the app.
  • An error in an observer will cause the current session to terminate. If you don’t want this to happen, you’ll need to wrap the code in try() or tryCatch().

This same system powers req() (), which emits a special type of error. This special error causes observers and outputs to stop what they’re doing but not otherwise fail. By default, it will cause outputs to reset to their initial blank state, but if you use req(..., cancelOutput = TRUE) they’ll preserve their current display.

15.2.2 on.exit()

You can think of reactive(x()) as a shortcut for function() x(), automatically adding laziness and caching. This is mostly of importance if you want to understand how Shiny is implemented, but means that you can use functions that only work inside functions. The most useful of these is on.exit() which allows you to run code when a reactive expression finishes, regardless of whether the reactive successfully returns an error or fails with an error. This is what makes on.exit() work in .

15.2.3 Exercises

15.2.3.1 Error propagation with stop()

Use the {reactlog} package to observe an error propagating through the reactives in the following app, confirming that it follows the same rules as value propagation.

R Code 15.5 : Error propagation

Listing / Output 15.7: Analyzing the error propagation using stop() with {reactlog}
Loading...




I added the line reactlog::reactlog_enable() to the app code, started the app and ticked the check box “error?”. In the console I received

Warning: Error in <reactive:a>: Error

I started {reactlog} with the Cmd-F3 shortcut and analyzed the propagation. I could confirm that the error followed the same rules as the value.

15.2.3.2 Error propagation with req()

Modify the above app to use req() instead of stop(). Verify that events still propagate the same way. What happens when you use the cancelOutput argument?

R Code 15.6 : Error propagation

Listing / Output 15.8: Analyzing the error propagation using req() with {reactlog}
Loading...




  • Using req() in the if() condition does not 3 as in out doesn’t output anything. As the condition req() = TRUE is not fulfilled Shiny stops the execution.
  • Using req(…, cancelOutput = TRUE) shows for me no difference. It is said the in the documentation that

it is treated slightly differently if one or more outputs are currently being evaluated. In those cases, the reactive chain does not proceed or update, but the output(s) are left is whatever state they happen to be in (whatever was their last valid state).

But in the reactlog it shows me the new condition (input$error 'logo TRUE). But reading the next pargraph in the docs gives an explanation:

Note that this is always going to be the case if this is used inside an output context (e.g. output$txt <- ...). It may or may not be the case if it is used inside a non-output context (e.g. reactive(), observe() or observeEvent()) — depending on whether or not there is an output$... that is triggered as a result of those calls. (emphasis is mine)

15.3 Observers and Outputs

Observers and outputs are terminal nodes in the reactive graph. They differ from reactive expressions in two important ways:

  • They are eager and forgetful — they run as soon as they possibly can and they don’t remember their previous action. This eagerness is “infectious” because if they use a reactive expression, that reactive expression will also be evaluated.
  • The value returned by an observer is ignored because they are designed to work with functions called for their side-effects, like cat() or write.csv().

Observers and outputs are powered by the same underlying tool: observe(). This sets up a block of code that is run every time one of the reactive values or expressions it uses is updated. Note that the observer runs immediately when you create it — it must do this in order to determine its reactive dependencies.

R Code 15.7 : Testing behavior of observe()

Listing / Output 15.9: Nut run: Testing behavior of observe()
```{r}
#| label: observe-test
#| results: hold

y <- reactiveVal(10)
observe({
  message("`y` is ", y())
})

y(5)
y(4)
```

Hadley rarely uses observe() in this book, because it’s the low-level tool that powers the user-friendly observeEvent(). Generally, you should stick with observeEvent() unless it’s impossible to get it to do what you want. In this book, there is only one case where observe() is necessary. See

observe() also powers reactive outputs. Reactive outputs are a special type of observers that have two important properties:

  • They are defined when you assign them into output, i.e. output$text <- ... creates the observer.
  • They have some limited ability to detect when they’re not visible (i.e. they’re in non-active tab) so they don’t have to recompute.

It’s important to note that observe() and the reactive outputs don’t “do” something, but “create” something (which then takes action as needed). That mindset helps you to understand what’s going on in this example:

R Code 15.8 : observe() calls observe()

Listing / Output 15.10: observe() calls observe()
Code
x <- reactiveVal(1)
observe({
  x()
  observe(print(x()))
})
x(2)
x(3)
Caution 15.1

In contrast to the book example my observe() functions does not assign a value to y. This generates a > Warning: Error in y: could not find function “y”

Observers do not yield a result, they are only useful for their side effects.

Each change to x causes the observer to be triggered. The observer itself calls observe() setting up another observer. So each time x changes, it gets another observer, so its value is printed another time.

As a general rule, you should only ever create observers or outputs at the top-level of your server function. If you find yourself trying to nest them or create an observer inside an output, sit down and sketch out the reactive graph that you’re trying to create — there’s almost certainly a better approach. It can be harder to spot this mistake directly in a more complex app, but you can always use the reactlog: just look for unexpected churn in observers (or outputs), then track back to what is creating them.

15.4 Isolating code

To finish off the chapter we need to discuss two important tools for controlling exactly how and when the reactive graph is invalidated. In this section, we’ll discuss isolate(), the tool that powers observeEvent() and eventReactive(), and that lets you avoid creating reactive dependencies when not needed. In the next section, you’ll learn about invalidateLater() which allows you to generate reactive invalidations on a schedule.

15.4.1 isolate()

R Code 15.9 : Observer with infinite loop

Listing / Output 15.11: Not run: Observer with infinite loop
Code
r <- reactiveValues(count = 0, x = 1)
observe({
  r$x
  r$count <- r$count + 1
  on.exit()
  # if (r$count > 10) stop("To many calls") # my addition
  print(r$count)                          # my addition
})

If you were to run , you’d immediately get stuck in an infinite loop because the observer will take a reactive dependency on x and count; and since the observer modifies count, it will immediately re-run.

I tried to run with two addition: The first one tries to stop the loop after 10 rounds, the second one prints the result for each run.

But this didn’t work. To stop a running process in Shiny is a complex enterprise as I have found out. Searching on the Internet I learned about some pointers to solve this problem. But here I have skipped this non-trivial learning task and prevented to run the infinite loop with eval = false

Resource 15.1 : How to stop running Shiny processes without killing the app

  • StackOverflow answer with some important references
  • ExtendendTask is a relatively new Shiny function about running long tasks in the background, which could possible also address the stop problem of Shiny process.

Additionally I found reference to other packages which could use to stop running processes:

  • {callr}: Call R from R
  • {future}: Unified Parallel and Distributed Processing in R for Everyone
  • {ipc}: Tools for Message Passing Between Processes
  • {mirai}: Minimalist Async Evaluation Framework for R

Shiny provides isolate() to resolve the problem of undesired reactive dependency. This function allows you to access the current value of a reactive value or expression without taking a dependency on it:

R Code 15.10 : Accessing reactive value without taking a dependency on it

Listing / Output 15.12: Using isolate(): Accessing reactive values or reactive expressions without taking a dependency on it
Code
r <- reactiveValues(count = 0, x = 1)
r$count 
#> [1] 0
Code
#> [1] "reactivevalues"
Code
observe({
  r$x
  r$count <- isolate(r$count) + 1
})

r$x <- 1
r$x <- 2
r$count
#> [1] 0
Code
r$x <- 3
r$count
#> [1] 0

Like observe(), a lot of the time you don’t need to use isolate() directly because there are two useful functions that wrap up the most common usage: observeEvent() and eventReactive().

15.4.2 observeEvent() and eventReactive()

When you saw the code above, you might have remembered and wondered why we didn’t use observeEvent():

R Code 15.11 : Replace observe() with observeEvent()

Listing / Output 15.13: To prevent an infinite loop with observe() in you could use isolate() as in , but also observeEvent()
Code
observeEvent(r$x, { 
  r$count <- r$count + 1 
}) 

Indeed, we could used observeEvent(x, y) because it is equivalent toobserve({x; isolate(y)}). It elegantly decouples what you want to listen to from what action you want to take. And eventReactive() performs the analogous job for reactives: eventReactive(x, y) is equivalent to reactive({x; isolate(y)}).

observeEvent() and eventReactive() have additional arguments that allow you to control the details of their operation:

  • By default, both functions will ignore any event that yields NULL (or in the special case of action buttons, 0). Use ignoreNULL = FALSE to also handle NULL values.
  • By default both functions will run once when you create them. Use ignoreInit = TRUE to skip this run.
  • For observeEvent() only, you can use once = TRUE to run the handler only once.

15.4.3 Exercises

15.4.3.1 Complete app

Complete the app below with a server function that updates out with the value of x only when the button is pressed.

R Code 15.12 : Complete app using isolate()

Listing / Output 15.14: App updates with the value of x only when the button is pressed
Loading...




15.5 Timed invalidation

isolate() reduces the time the reactive graph is invalidated. This topic of this section, invalidateLater() does the opposite: it lets you invalidate the reactive graph when no data has changed. You saw an example of this in with reactiveTimer(), but the time has come to discuss the underlying tool that powers it: invalidateLater().

invalidateLater(ms) causes any reactive consumer to be invalidated in the future, after ms milliseconds. It is useful for creating animations and connecting to data sources outside of Shiny’s reactive framework that may be changing over time. For example, this reactive will automatically generate 10 fresh random normals every half a second:

R Code 15.13 : Observer incrementing number

Listing / Output 15.15: Observer increments a cumulative sum with a random number and stops with number over 10
Loading...




In the following sections you’ll learn how to use invalidateLater() to read changing data from disk, how to avoid getting invalidateLater() stuck in an infinite loop, and some occasionally important details of exactly when the invalidation happens.

15.5.1 Polling

A useful application of invalidateLater() is to connect Shiny to data that is changing outside of R. For example, you could use the following reactive to re-read a csv file every second:

Listing / Output 15.16: Not run: Re-reading a CSV file with invalidateLater()
```{r}
data <- reactive({
  on.exit(invalidateLater(1000))
  read.csv("data.csv")
})
```

This connects changing data into Shiny’s reactive graph, but it has a serious downside: when you invalidate the reactive, you’re also invalidating all downstream consumers, so even if the data is the same, all the downstream work has to be redone.

To avoid this problem, Shiny provides reactivePoll() which takes two functions: one that performs a relatively cheap check to see if the data has changed and another more expensive function that actually does the computation. We can use reactivePoll() to rewrite the previous reactive as below.

Listing / Output 15.17: Not run: Re-reading a CSV file with reactivePoll()
```{r}
server <- function(input, output, session) {
  data <- reactivePoll(1000, session, 
    function() file.mtime("data.csv"),
    function() read.csv("data.csv")
  )
}
```

Here we used to file.mtime(), which returns the last time the file was modified, as a cheap check to see if we need to reload the file:

Reading a file when it changes is a common task, so Shiny provides an even more specific helper with reactiveFileReader() that just needs a file name and a reader function:

Listing / Output 15.18: Not run: Re-reading a CSV file with reactiveFileReader()
```{r}
server <- function(input, output, session) {
  data <- reactiveFileReader(1000, session, "data.csv", read.csv)
}
}
```

If you need to read changing data from other sources (e.g. a database), you’ll need to come up with your own reactivePoll() code.

15.5.2 Long running reactives

If you’re performing a long running computation, there’s an important question you need to consider: when should you execute invalidateLater()? For example, take this reactive:

Listing / Output 15.19: Not run: Long running reactive with invalidateLater() time problem.
```{r}
server <- function(input, output, session) {
  data <- reactiveFileReader(1000, session, "data.csv", read.csv)
}
```

Assume Shiny starts the reactive running at time 0, it will request invalidation at time 500. The reactive takes 1000ms to run, so it’s now time 1000, and it’s immediately invalidated and must be recomputed, which then sets up another invalidation: we’re stuck in an infinite loop.

On the other hand, if you run invalidateLater() at the end, it will invalidate 500ms after completion, so the reactive will be re-run every 1500 ms.

Listing / Output 15.20: Not run: Long running reactive with invalidateLater() time problem solved.
```{r}
x <- reactive({
  on.exit(invalidateLater(500), add = TRUE)
  Sys.sleep(1)
  10
})
```

This is the main reason to prefer invalidateLater() to the simpler reactiveTimer() that we used earlier: it gives you greater control over exactly when the invalidation occurs.

15.5.3 Timer accuracy

The number of milliseconds specified in invalidateLater() is a polite request, not a demand. R may be doing other things when you asked for invalidation to occur, so your request has to wait. This effectively means that the number is a minimum and invalidation might take longer than you expect. In most cases, this doesn’t matter because small differences are unlikely to affect user perception of your app. However, in situations where many small errors will accumulate, you should compute the exact elapsed time and use it to adjust your calculations.

For example, the following code computes distance based on velocity and elapsed time. Rather than assuming invalidateLater(100) always delays by exactly 100 ms, we’ll compute the elapsed time and use it in the calculation of position.

Listing / Output 15.21: Not run: Computing elapsed time for timer accuracy
```{r}
velocity <- 3
r <- reactiveValues(distance = 1)

last <- proc.time()[[3]]
observe({
  cur <- proc.time()[[3]]
  time <- last - cur
  last <<- cur
  
  r$distance <- isolate(r$distance) + velocity * time
  invalidateLater(100)
})
```

15.5.4 Exercises

15.5.4.1 Why reactive never executed?

Why will this reactive never be executed? Your explanation should talk about the reactive graph and invalidation.

R Code 15.14 : Exercise: A reactive never executed

Listing / Output 15.22: Exercise: The reactive is never executed because there is no output/observer and input/value to call the reactive expression invalidate. Shiny can’t therefore start (re)-execution.
Loading...




The reactive is never called. There is no input that can be invalidated and on the other hand, there is no reactive consumer (an output) to display changed values.

After initializing a session, Shiny can’t pick an observer because there is none. As Shiny does not choose a reactive expression for the execution start, and there is no input that could be invalidated, the reactive can’t never be called.

To see the difference I have complemented the app in with an output. Now you can see that the reactive is executed.

R Code 15.15 : Exercise: The reactive is this time executed

Listing / Output 15.23: Exercise: The reactive can now be executed, because Shiny can invalidate the output/observer and the reactive can be called.
Loading...




Caution 15.2: stopApp() not working with shinylive-r

I had to use a more complex logic with an intermediate reactive value because the simpler solution with stopApp() inside an observeEvent() listening to the “stop” actionButton() does not work with shinylive-r.

15.5.4.2 reactivePoll() with SQL

If you’re familiar with SQL, use reactivePoll() to only re-read an imaginary “Results” table whenever a new row is added. You can assume the Results table has a timestamp field that contains the date-time that a record was added.

As I am not familiar with SQL I will skip this exercise.

15.6 Glossary Entries

term definition
SQL Structured Query Language (SQL) (pronounced ess-kew-ell or sequel) is a standardized programming language that is used to manage relational databases and perform various operations on the data in them. Initially created in the 1970s, SQL is regularly used not only by database administrators but also by developers writing data integration scripts and data analysts looking to set up and run analytical queries. For more information see task view on databases <https://cran.r-project.org/web/views/Databases.html>.

Session Info

Session Info

Code
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.1 (2025-06-13)
#>  os       macOS Sequoia 15.5
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Europe/Vienna
#>  date     2025-07-17
#>  pandoc   3.7.0.2 @ /opt/homebrew/bin/ (via rmarkdown)
#>  quarto   1.8.4 @ /usr/local/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version     date (UTC) lib source
#>  cli            3.6.5       2025-04-23 [1] CRAN (R 4.5.0)
#>  commonmark     2.0.0       2025-07-07 [1] CRAN (R 4.5.0)
#>  crayon         1.5.3       2024-06-20 [1] CRAN (R 4.5.0)
#>  curl           6.4.0       2025-06-22 [1] CRAN (R 4.5.0)
#>  dichromat      2.0-0.1     2022-05-02 [1] CRAN (R 4.5.0)
#>  digest         0.6.37      2024-08-19 [1] CRAN (R 4.5.0)
#>  evaluate       1.0.4       2025-06-18 [1] CRAN (R 4.5.0)
#>  farver         2.1.2       2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0       2024-05-15 [1] CRAN (R 4.5.0)
#>  glossary     * 1.0.0.9003  2025-06-08 [1] local
#>  glue           1.8.0       2024-09-30 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1     2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4       2023-12-06 [1] CRAN (R 4.5.0)
#>  httpuv         1.6.16      2025-04-16 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0       2025-03-27 [1] CRAN (R 4.5.0)
#>  kableExtra     1.4.0       2024-01-24 [1] CRAN (R 4.5.0)
#>  knitr          1.50        2025-03-16 [1] CRAN (R 4.5.0)
#>  later          1.4.2       2025-04-08 [1] CRAN (R 4.5.0)
#>  lifecycle      1.0.4       2023-11-07 [1] CRAN (R 4.5.0)
#>  litedown       0.7         2025-04-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.3       2022-03-30 [1] CRAN (R 4.5.0)
#>  markdown       2.0         2025-03-23 [1] CRAN (R 4.5.0)
#>  mime           0.13        2025-03-17 [1] CRAN (R 4.5.0)
#>  promises       1.3.3       2025-05-29 [1] CRAN (R 4.5.0)
#>  R6             2.6.1       2025-02-15 [1] CRAN (R 4.5.0)
#>  RColorBrewer   1.1-3       2022-04-03 [1] CRAN (R 4.5.0)
#>  Rcpp           1.1.0       2025-07-02 [1] CRAN (R 4.5.0)
#>  rlang          1.1.6       2025-04-11 [1] CRAN (R 4.5.0)
#>  rmarkdown      2.29        2024-11-04 [1] CRAN (R 4.5.0)
#>  rstudioapi     0.17.1      2024-10-22 [1] CRAN (R 4.5.0)
#>  rversions      2.1.2       2022-08-31 [1] CRAN (R 4.5.0)
#>  scales         1.4.0       2025-04-24 [1] CRAN (R 4.5.0)
#>  sessioninfo    1.2.3       2025-02-05 [1] CRAN (R 4.5.0)
#>  shiny        * 1.11.1.9000 2025-07-08 [1] Github (rstudio/shiny@f752856)
#>  stringi        1.8.7       2025-03-27 [1] CRAN (R 4.5.0)
#>  stringr        1.5.1       2023-11-14 [1] CRAN (R 4.5.0)
#>  svglite        2.2.1       2025-05-12 [1] CRAN (R 4.5.0)
#>  systemfonts    1.2.3       2025-04-30 [1] CRAN (R 4.5.0)
#>  textshaping    1.0.1       2025-05-01 [1] CRAN (R 4.5.0)
#>  vctrs          0.6.5       2023-12-01 [1] CRAN (R 4.5.0)
#>  viridisLite    0.4.2       2023-05-02 [1] CRAN (R 4.5.0)
#>  xfun           0.52        2025-04-02 [1] CRAN (R 4.5.0)
#>  xml2           1.3.8       2025-03-14 [1] CRAN (R 4.5.0)
#>  xtable         1.8-4       2019-04-21 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10      2024-07-26 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/library
#>  [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────