richarddmorey
richarddmorey

Reputation: 1160

prevent double execution of shiny reactive with two dependency pathways

Consider the following shiny app. There are three basic inputs, that the user can change: A, M, and S. The "content" C (in the verbatimTextOuput on the right) depends directly on A and S.

enter image description here

S can be changed in two ways: by the user, or by changing M/A. If the user changes S, then the dependency on M is irrelevant. M is also not used if it is empty.

The situation is depicted in the diagram below.

Diagram of dependencies

The problem is when M is not blank and A is changed:

  1. C gets updated based on the old S and new A
  2. S gets updated based on M and the new A
  3. C gets updated based on the new S and new A.

Thus, C gets updated twice, the first time with an invalid value.

What I want to happen is for S to update, based on the new A, then C to update based on the new S and new A.

To see the problem, run the app, then:

  1. Put something in the M box
  2. Change A
  3. Observe that the C is changed twice.

How can I block the first update?

Thanks!


Shiny app code:

library(shiny)
library(digest)

ui <- fluidPage(

    sidebarLayout(
        sidebarPanel(
           textInput("M", "M:", ""), 
           textInput("S", "S:", "testS"),
           selectInput("A", "A:", c("A1","A2"))
        ),
        
        mainPanel(
           verbatimTextOutput("C")
        )
    )
)

server <- function(input, output, session) {
    
    # Count calculations of C
    count = 0
    
    # Make reactive so we can modify the value
    # in the input box (inpt$S is an input and output,
    # essentially)
    S <- reactive({
        input$S
    })
    
    # Create "content" from A and S
    C <- reactive({
            count <<- count + 1
            Sys.sleep(0.5)
            message("Count ", count)
            paste(
                "Count: ", count, "\n", digest::sha1( c(input$A, S()) )
                )
    })

    # When M changes, we need to change S based on A and M
    # OR set S to a default value
    observeEvent(input$M, {
        
        # If user gets rid of M, reset S to default
        if(input$M == ""){
            S = "testS"
        }else{
            S = digest::sha1(c(input$M,input$A))
        }
        
        # Update the input to new S
        updateTextInput(
            session, 
            "S",
            value = S
        )
    })

    # When A changes, we need to change S based on A and M
    # OR if M is blank, do nothing (S doesn't depend on M if M is blank)
    observeEvent(input$A, {
    
        # If there's no M, don't use it
        if(input$M == "") return()
        
        # Update the input to new S
        updateTextInput(
            session, 
            "S",
            value = digest::sha1(c(input$M,input$A))
        )
    })
    
    # "Content"
    output$C <- renderText({
        C()
    })
    
}

shinyApp(ui = ui, server = server)

Upvotes: 1

Views: 514

Answers (2)

Lisa DeBruine
Lisa DeBruine

Reputation: 878

This should work (if I've understood the logic of your function). It has a bunch of extra messages so you can see what is updated by when and in what order.

(I also changed the superassignment of count because those just bug me, but I get the it's a little awkward with the isolate, so feel free to put it back ;)

library(shiny)
library(digest)

ui <- fluidPage(
  
  sidebarLayout(
    sidebarPanel(
      textInput("M", "M:", ""), 
      textInput("S", "S:", "testS"),
      selectInput("A", "A:", c("A1","A2"))
    ),
    
    mainPanel(
      verbatimTextOutput("C")
    )
  )
)

server <- function(input, output, session) {
  # Count calculations of C
  count = reactiveVal(0)
  S = reactiveVal("testS")
  
  observeEvent(input$S, { message("S updated externally")
    S(input$S)
  })
  
  # When M changes, we need to change S based on A and M
  # OR set S to a default value
  observeEvent(input$M, { message("M updated")
    # If user gets rid of M, reset S to default
    if (input$M == ""){
      S("testS")
    } else { 
      S(digest::sha1(c(input$M, input$A)))
    }
    
    # Update the input to new S 
    updateTextInput(inputId =  "S", value = S())
    message("S updated by M")
  })
  
  # When A changes, we need to change S based on A and M
  # OR if M is blank, do nothing (S doesn't depend on M if M is blank)
  observeEvent(input$A, { message("A updated")
    # If there's no M, don't use it
    req(input$M)
    
    # Update the input to new S
    S(digest::sha1(c(input$M, input$A)))
    updateTextInput(inputId =  "S", value = S())
    message("S updated by A")
  })
  
  # "Content"
  output$C <- renderText({ 
    n = isolate(count()) + 1
    count(n)
    #Sys.sleep(0.5)
    message("Count ", n)
    
    paste("Count: ", n, "\n", digest::sha1( c(input$A,  S())))
  })
}

shinyApp(ui = ui, server = server)

The S() reactiveVal updates when the user changes input$S externally or internally by input$M or input$A. The comments show which of those changes the value and output$C only changes when input$A or the S() change.

Upvotes: 1

Shorpy
Shorpy

Reputation: 1579

I think the issue here is that you are using observers when you should use reactives. In general you only want to use observers for side effects (saving a file, pushing a button) not when you want a value in the app. Here I think it's better to use renderUI to generate the UI element reactively rather than updating it with the observer.

library(shiny)
library(digest)

ui <- fluidPage(
  
  sidebarLayout(
    sidebarPanel(
      textInput("M", "M:", ""), 
      uiOutput("S_UI"),
      selectInput("A", "A:", c("A1","A2"))
    ),
    
    mainPanel(
      verbatimTextOutput("C")
    )
  )
)

server <- function(input, output, session) {
  
  # Count calculations of C
  count = 0

  # Create "content" from A and S
  C <- reactive({
    count <<- count + 1
    Sys.sleep(0.5)
    message("Count ", count)
    paste(
      "Count: ", count, "\n", digest::sha1( c(input$A, input$S ))
    )
  })
  
  output$S_UI <- renderUI({
    if (input$M == "") {
      val <- "testS"
    } else {
      val <- "S"
    }
    return(textInput("S", "S:", val))
  })
  
  # "Content"
  output$C <- renderText({
    C()
  })
}

shinyApp(ui = ui, server = server)

Upvotes: 1

Related Questions