R Shiny - Understanding the difference between observe and observeEvent when updating mutually dependent inputs

Viewed 1935

The app below has two mutually dependent numericInputs, a and b. The value of input$a is 1-input$b and the value of input$b is 1-input$a. Whenever the user changes an input's value, I would like to update the value of the other accordingly. The code below contains two approaches:

  1. A single observer
  2. Two separate observers - one that listens for changes to a and updates b and another that listens to b and updates a.

When I try 1), the inputs get stuck in an infinite loop but I'm not sure I understand why. If the user changes b, then the observer is triggered and the value of a gets updated to be 1-b. Since the values of both inputs are now up-to-date, there are no further changes to input$a or input$b and the observer shouldn't be triggered again until the user makes another change. But this isn't the case.

How exactly does approach 1) differ from approach 2)? Why does 1) get stuck in a loop whereas 2) doesn't?

library(shiny)

shinyApp(
  ui = fluidPage(
    numericInput('a','a', value = 0.5, max = 1, min = 0),
    numericInput('b','b', value = 0.5, max = 1, min = 0)
  ),
  server = function(input, output, session) {

    # Using a single observer ----
    observe({

      updateNumericInput(session, 'a', value = 1-req(input$b))

      updateNumericInput(session, 'b', value = 1-req(input$a))

      print(input$a)

    })

    # Using separate observers ----
    # observeEvent(input$b, {
    #   updateNumericInput(session, 'a', value = 1-input$b)
    # })
    # 
    # observeEvent(input$a, {
    #   updateNumericInput(session, 'b', value = 1-input$a)
    # })

  }
)
2 Answers

In the documentation of observe and observeEvent, it is written:

  • observe

An observer is like a reactive expression in that it can read reactive values and call reactive expressions, and will automatically re-execute when those dependencies change. [...] [O]bservers use eager evaluation; as soon as their dependencies change, they schedule themselves to re-execute.

In your example, observe never stops "observing" and updates the input as soon as the other changes, even if the user doesn't do anything. Therefore, you stay in an infinite loop since each of the two inputs depends on the other.

  • observeEvent

[S]ometimes you want to wait for a specific action to be taken from the user, like clicking an actionButton(), before calculating an expression or taking an action. [...] Use observeEvent whenever you want to perform an action in response to an event.

Therefore, observeEvent needs the user to make an action to run the function inside. In your example, input "a" will be updated only if the user manually changes input "b" and vice-versa. That's why observeEvent works in this scenario and observe does not.

Edit: to have clearer results of the loop created when using observe, here's a slightly modified version of the example in the OP:

library(shiny)

shinyApp(
  ui = fluidPage(
    numericInput('a','a', value = NULL, max = 1, min = 0),
    numericInput('b','b', value = NULL, max = 1, min = 0)
  ),
  server = function(input, output, session) {

    # Using a single observer ----
    observe({

      updateNumericInput(session, 'a', value = 1-req(input$b))

      updateNumericInput(session, 'b', value = 1-req(input$a))

      print(paste0("a = ", input$a))
      print(paste0("b = ", input$b))

    })


    # Using separate observers ----
    # observeEvent(input$b, {
    #   updateNumericInput(session, 'a', value = 1-input$b)
    # })
    # 
    # observeEvent(input$a, {
    #   updateNumericInput(session, 'b', value = 1-input$a)
    # })

  }
)

Careful with the interpretation: the beginning values of a and b are printed. Then, the new values of a and b are created but the new value of b depends on the first value of a (not on the second value of a). The third values of a and b will depend on the second values of a and b, etc.

In first case the observer looks at all its dependencies, here input$a and input$b. If anything changes it fires its code. Now, when a or b changes the observer updates numericInputs. But it notifies the observer that its dependencies changed and it has to run again. That's why you end up with an infinite loop.

When you use observeEvents it's a little bit different. Suppose you set a = 1. One of the observers is triggered and it updates b, so b = 0. Now, the other observer is triggered and it updates a, so a = 1. But in this case, the new value is exactly the same as the old one, so internal mechanisms of reactivity in shiny stops it from invalidating a again since it would be redundant.

Related