Best practices for validating user input with an `actionButton` and `eventReactive`

Hello all,

I've got a question about best practices when it comes to value input validation in combination with an actionButton in an observeEvent -> eventReactive environment. I would like to check the values entered by the user according to certain conditions when the actionButton is clicked. If the conditions are not satisfied an error message should appear and the function connected to the actionButton should not be executed. Correcting the input value and clicking the actionButton again should trigger the function (and display a modal box in this case).

I made the following replex to represent the code I'm working with:

library(shiny)
suppressMessages(library(shinyalert))

ui <- fluidPage(
   
   useShinyalert(),
   
   titlePanel("Validation/actionButton/eventReactive replex"),
   
   sidebarLayout(
      sidebarPanel(
      p("Clicking the button without changing the value results in an error messages and suppresses the modal popup. Change the value to get the popup and see the object returned by shinyalert.")
      ),
      
      mainPanel(
        numericInput("test",
                   h4("Test input"),
                   value=2700,
                   min=0),
      verbatimTextOutput("test_box"),
      
      actionButton("go","Execute")
      
      )
   )
)

server <- function(input, output) {
   v <- eventReactive(input$go, {
      
      validate(
        need(input$test != 2700, "This value can't be 2700")
      )
     
     shinyalert(
        title = "The value you entered is:",
        text = input$test,
        type = "success"
      )
   })
   
   output$test_box <- renderText({
     v()
   })
}

shinyApp(ui = ui, server = server)

This replex works but I was wondering if there are best practices that I am not aware of that I could use to improve this code. For example, the error validation and the shinyalert function are in the same eventReactive environment which causes the modal box that pops up when the actionButton is clicked and all conditions are satisfied to send its return value to the textOutput that I'm using to display the error messages. I haven't found a way to separate them and to not have the return value displayed when the modal pops up. I tried isolate as well but to no avail.

I looked through the forums before posting but I couldn't find an answer to this. As I am relatively new to Shiny so I hope that I did not miss anything in the documentation and that this is a valid question to ask. In either case, thank you for your help!

Regarding the best practice:

Usually validate is directly placed in render* functions - this also avoids the unwanted eventReactive output:

library(shiny)
library(shinyalert)

ui <- fluidPage(
  useShinyalert(),
  titlePanel("Validation/actionButton/eventReactive replex"),
  sidebarLayout(
    sidebarPanel(
      p(
        "Clicking the button without changing the value results in an error messages and suppresses the modal popup. Change the value to get the popup and see the object returned by shinyalert."
      )
    ),
    mainPanel(
      numericInput("test",
                   h4("Test input"),
                   value = 2700,
                   min = 0),
      verbatimTextOutput("test_box"),
      actionButton("go", "Execute")
    )
  )
)

server <- function(input, output) {
  observeEvent(input$go, {
    if(input$test != 2700){
      shinyalert(title = "The value you entered is:",
                 text = input$test,
                 type = "success")
    }
  })
  
  output$test_box <- renderText({
    validate(need(input$test != 2700, "This value can't be 2700"))
  })
}

shinyApp(ui = ui, server = server)

Personally I think, opening a modal after the user enters an unsuitable value is a little too much - but it depends on the usecase I guess.

You might want to check out RStudio's shinyvalidate package, which marks the invalid inputs red to notify the user:

Here is an alternative implementation using shinyvalidate:


library(shiny)
library(shinyvalidate)

ui <- fluidPage(
  titlePanel("Validation/actionButton/eventReactive replex"),
  sidebarLayout(
    sidebarPanel(),
    mainPanel(
      numericInput("test",
                   h4("Test input"),
                   value = 2700,
                   min = 0),
      actionButton("go", "Execute")
    )
  )
)

server <- function(input, output) {
  iv <- InputValidator$new()
  iv$add_rule("test", sv_integer())
  iv$add_rule("test", sv_not_equal(2700))
  iv$enable()
  
  observeEvent(input$go, {
    req(iv$is_valid())
    showNotification("This triggered something useful.", type = "message")
  })
}

shinyApp(ui = ui, server = server)

Hi @ismirsehregal,

Thanks a lot for your quick and very useful reply/explanation and alternative implementation (with an animation even!)!! It helped me greatly to better understand the concept of validation and what is considered best practice. It makes sense to place the validate directly in the render* function. I also agree with you that opening a model to alert the user of invalid input is overkill. The way shinyvalidate handles this is much preferable.

Your shinyvalidate implementation is very elegant and pretty much what I was looking for. The package seems to make it very easy to include multiple validation conditions which is something I need as well. You thankfully already showed me how to do that by adding an extra rule to the InputValidator function. I can't remember exactly but I believe that I came across the package before. I think that I foolishly abandoned it too soon because I had trouble writing custom validation checks and error messages. After seeing your implementation, however, I will give it another try.

One thing that I don't understand yet (but will surely be able to figure out with the help of your excellent answer) is how to only show the error message when the button is clicked i.e. running the validation checks at the click of the button only instead of constantly checking the input. The value placeholder in the numericInput is the value that will not be allowed to be entered but it shouldn't be marked as invalid input when the app is opened for the first time. In other words, I will have to find a way to not run the function that is triggered by the button unless all validation checks are passed but only show an error message to alert of invalid input if the button is pressed. This is also why I used an eventReactive function in my original replex. I'll update this questions in case I find a solution.

Thank you again for your time and effort @ismirsehregal - I greatly appreciate it!

Thanks for your feedback!

To be honest, I don't see a situation where the behaviour you are describing is advantageous.

If the user gets notified regarding invalid input only after a button click, he/she needs to navigate back to the input element. This might get annoying especially with multiple inputs.

I'd give the warning as long as the user focuses on that input.

If you want to avoid red-marked inputs on startup you may pass valid initial values.

However, if you still want to check only after the button was clicked you can use iv$enable() and iv$disable() to control the validation:

library(shiny)
library(shinyvalidate)

ui <- fluidPage(
  titlePanel("Validation/actionButton/eventReactive replex"),
  sidebarLayout(
    sidebarPanel(),
    mainPanel(
      numericInput("test",
                   h4("Test input"),
                   value = 2700,
                   min = 0),
      actionButton("go", "Execute")
    )
  )
)

server <- function(input, output) {
  iv <- InputValidator$new()
  iv$add_rule("test", sv_integer())
  iv$add_rule("test", sv_not_equal(2700))
  
  
  observeEvent(input$go, {
    iv$enable()
    req(iv$is_valid())
    showNotification("This triggered something useful.", type = "message")
  })
  
  observeEvent(input$test, {
    iv$disable()
  })
}

shinyApp(ui = ui, server = server)