16  Escaping the graph

Table of content for chapter 16

Chapter section list

16.1 Introduction

In this chapter you’ll learn how you can combine reactiveValues() and observe()/observeEvent() to connect the right hand side of the reactive graph back to the left hand side. These techniques are powerful because they give you manual control over parts of the graph. But they’re also dangerous because they allow your app to do unnecessary work. Most importantly, you can now create infinite loops where your app gets stuck in a cycle of updates that never ends.

16.2 What doesn’t the reactive graph capture?

In Section Section 14.3.12 we discussed what happens when the user causes an input to be invalidated. There are two other important cases where you as the app author might invalidate an input:

  • You call an update function setting the value argument. This sends a message to the browser to change the value of an input, which then notifies R that the input value has been changed.
  • You modify the value of a reactive value (created with reactiveVal() or reactiveValues()).

It’s important to understand that in both of these cases a reactive dependency is not created between the reactive value and the observer. While these actions cause the graph to invalidate, they are not recorded through new connections.

To make this idea concrete, take the following simple app, with reactive graph shown in Figure 16.1.

The input 'nm' is connected with the reactive 'hi' which in turn is connected with the output 'hi'. In another line below the input 'clr' is connected with an unnamed observer.
Picture 16.1: The reactive graph does not record the connections when update functions or reactive values invalidate inputs. Therefore there is no connection between the unnamed observer and the nm input; this dependency is outside of Shiny’s scope.

Procedure 16.1 : What happens when you press the clear button in Listing / Output 16.1?

  1. input$nm invalidates, invalidating hi() then output$hi (steps 13-15 invalidating; 16-17 removing dependencies) [book: step 4]
  2. output$hi recomputes, forcing hi() to recompute (steps 18-20) [book: step 5]
  3. The browser changes the value of nm (21-23; 24 idle) [book: step 3]
  4. input$clr invalidates, which then invalidates the observer (steps 25-27). [book: step 1]
  5. The observer recomputes, recreating the dependency on input$clr, and telling the browser to change the value of the input control (steps 28-33, idle 34). [book: step 2]

None of these actions change the reactive graph, so it remains as in Picture 16.1 and the graph does not capture the connection from the observer to input$nm.

Note 16.1: {reactlog} steps and sequences

As a debugging aid, the {reactlog} package can capture and draw these connection when you modify reactive values from an observer, but this information is not used by Shiny. You can see that connection with the unnamed observer as a parallel line below the input nm., but there is no connection between unnamed observer and input nm.

When you run Listing / Output 16.1, input a name, pressing the clear button and then invoking {reactlog} with CMD-F3 you can follow the procedure in Procedure 16.1. I have added the step numbers to the five mentioned actions. It turned out that there is in relation to the book a slightly change and different starting point in the order of the steps. If have put the book sequence in square brackets.

16.3 Case studies

Next, lets take a look at a few useful cases where you might combine reactiveValues() and observeEvent() or observe() to solve problems that are otherwise very challenging (if not impossible). These are useful templates for your own apps.

16.3.1 One output modified by multiple inputs

To get started we’ll tackle a very simple problem: @ a common text box that’s updated by multiple events.

Things get slightly more complicated in the next example, where we have an app with two buttons that let you increase and decrease values. We use reactiveValues() to store the current value, and then observeEvent() to increment and decrement the value when the appropriate button is pushed. The main additional complexity here is that the new value of r$n depends on the previous value.

Picture 16.2 shows the reactive graph for this example. Again note that the reactive graph does not include any connection from the observers back to the reactive value n.

alt-text
Picture 16.2: The reactive graph does not capture connections from observers (and reactive values) to input values

16.3.2 Accumulating inputs

It’s a similar pattern if you want to accumulate data in order to support data entry. Here the main difference is that we use updateTextInput() to reset the text box after the user clicks the add button.

We could make this slightly more useful by providing a delete button and making sure that the add button doesn’t create duplicate names:

16.3.3 Pausing animations

Another common use case is to provide a start and stop button that lets you control some recurring event. This example uses a running reactive value to control whether or not the number increments, and invalidateLater() to ensure that the observer is invalidated every 250 ms when running.

Note 16.2: Compare with my solution to stop shinylive-r

My solution to stop shinylive-r is very similar. See Listing / Output 15.23 and Caution 15.2.

Notice in this case we can’t easily use observeEvent() because we perform different actions depending on whether running() is TRUE or FALSE. Since we can’t use observeEvent(), we must use isolate() — if we don’t this observer would also take a reactive dependency on n, which it updates, so it would get stuck in an infinite loop.

Hopefully these examples start to give you a flavor of what programming with reactiveValues() and observe() feels like. It’s very imperative: when this happens, do that; when that happens, do the other thing. This makes it easier to understand on a small scale, but harder to understand when bigger pieces start interacting. So generally, you’ll want to use this as sparingly as possible, and keep it isolated so that the smallest possible number of observers modify the reactive value.

16.3.4 Exercises

16.3.4.1 Two histograms with two actionButtons

16.3.4.2 Two histograms with an actionButton and s selectInputs

16.3.4.3 Two histograms with only using reactive()

16.4 Anti-pattern

Once you get the hang of this pattern it’s easy to fall into bad habits:

alt-text
Picture 16.3: Using reactive values and observers leaves part of the graph disconnected

In this simple case, this code doesn’t do much extra work compared to the alternative that uses reactive():

alt-text
Picture 16.4: Using a reactives makes the dependencies between the components very clear.

Listing / Output 16.7 is not only more complex, but has other drawbacks too:

  • If the table or plot are in tabs that are not currently visible, the observer will still draw/plot them.
  • If the head() functions throws an error, the observe() will terminate the app, but reactive() will propagate it so it’s displayed.

And things will get progressively worse as the app gets more complicated. It’s very easy to revert to the event-driven programming situation described in Section 13.2.3. You end up doing a lot of hard work to analyse the flow of events in your app, rather than relying on Shiny to handle it for you automatically.

It’s informative to compare the two reactive graphs.

Picture 16.3 shows the graph from the first example. It’s misleading because it doesn’t look like nrows is connected to df(). Using a reactive, as in Picture 16.4, makes the precise connection easy to see. Having a reactive graph that is as simple as possible is important for both humans and for Shiny. A simple graph is easier for humans to understand, and a simple graph is easier for Shiny to optimize.

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-18
#>  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)
#>  curl          6.4.0      2025-06-22 [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)
#>  fastmap       1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
#>  glossary    * 1.0.0.9003 2025-06-08 [1] local
#>  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)
#>  jsonlite      2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
#>  knitr         1.50       2025-03-16 [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)
#>  sessioninfo   1.2.3      2025-02-05 [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)
#>  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.
#> 
#> ──────────────────────────────────────────────────────────────────────────────