Skip to content

Instantly share code, notes, and snippets.

@jonocarroll
Last active February 27, 2021 03:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jonocarroll/3fd80d80447476cdfec9b404fd08256d to your computer and use it in GitHub Desktop.
Save jonocarroll/3fd80d80447476cdfec9b404fd08256d to your computer and use it in GitHub Desktop.
How should non-reactive functions handle errors to be compatible with shiny?
## How should we write non-reactive (business logic) functions so that they
## are maximally compatible with shiny/reactive processing?
## Here's a function which performs business logic. It may be used at the
## console or it may be used in a shiny app.
## A shiny app will probably provide the right type of input so the
## input validations are perhaps not as critical, but a typical design
## is to stop() if a critical condition is not met during calculations.
var_top_cars <- function(d, minval) {
stopifnot("mpg not found" = utils::hasName(d, "mpg"))
d <- dplyr::filter(d, mpg >= minval)
stopifnot("no rows remaining, can't calculate hp variance" = nrow(d) > 0)
var(d$hp)
}
## this can be used at the console
var_top_cars(mtcars, 10) # works
var_top_cars(mtcars, 30) # works
var_top_cars(mtcars, 40) # error, can't calculate
var_top_cars(iris, 40) # error, bad input
library(shiny)
## this ui is common to all examples below
ui <- fluidPage(
titlePanel("hp Variance"),
sidebarLayout(
sidebarPanel(
sliderInput("min_mpg", "Minimum MPG:", min = 0, max = 50, value = 25)
),
mainPanel(
textOutput("variance")
)
)
)
## this app works because renderText() catches the error
server_simple <- function(input, output) {
output$variance <- renderText({
paste0("variance of top cars' hp = ", req(var_top_cars(mtcars, req(input$min_mpg))))
})
}
runApp(list(ui = ui, server = server_simple))
## this app *crashes* when the slider goes too high
## because the stop() occurs within reactive processing
## despite being in non-reactive code
server_complex <- function(input, output) {
out <- reactiveValues(v = NA)
res <- reactive({
req(var_top_cars(mtcars, req(input$min_mpg)))
})
observeEvent(req(input$min_mpg), {
out$v <- req(res())
})
output$variance <- renderText({
paste0("variance of top cars' hp = ", out$v)
})
}
runApp(list(ui = ui, server = server_complex))
## this non-reactive function has some shiny protection
## validate throws a classed error and shiny knows what to do with it
var_top_cars_shiny <- function(d, minval) {
validate(need(utils::hasName(d, "mpg"), "mpg not found"))
d <- dplyr::filter(d, mpg >= minval)
validate(need(nrow(d) > 0, "no rows remaining, can't calculate hp variance"))
var(d$hp)
}
## but it works in a non-reactive context just fine
var_top_cars_shiny(mtcars, 10) # works
var_top_cars_shiny(mtcars, 30) # works
var_top_cars_shiny(mtcars, 40) # error, can't calculate
var_top_cars_shiny(iris, 40) # error, bad input
## this app still works, as expected
server_simple_validate <- function(input, output) {
output$variance <- renderText({
paste0("variance of top cars' hp = ", req(var_top_cars_shiny(mtcars, req(input$min_mpg))))
})
}
runApp(list(ui = ui, server = server_simple_validate))
## now this app _does_ work (doesn't crash)
## admittedly, the output does not update if
## the slider goes too high
server_complex_validate <- function(input, output) {
out <- reactiveValues(v = NA)
res <- reactive({
req(var_top_cars_shiny(mtcars, req(input$min_mpg)))
})
observeEvent(req(input$min_mpg), {
out$v <- req(res())
})
output$variance <- renderText({
paste0("variance of top cars' hp = ", out$v)
})
}
runApp(list(ui = ui, server = server_complex_validate))
## another option would be to return(NULL) rather than stop
var_top_cars_ret <- function(d, minval) {
if (!utils::hasName(d, "mpg")) {
warning("mpg not found")
return(NULL)
}
d <- dplyr::filter(d, mpg >= minval)
if (!nrow(d) > 0) {
warning("no rows remaining, can't calculate hp variance")
return(NULL)
}
var(d$hp)
}
## this of course works in a non-reactive context
var_top_cars_ret(mtcars, 10) # works
var_top_cars_ret(mtcars, 30) # works
var_top_cars_ret(mtcars, 40) # returns NULL
var_top_cars_ret(iris, 40) # returns NULL
## this app works, but the value may be empty
## and the warning is at the developer's console
server_simple_null <- function(input, output) {
output$variance <- renderText({
paste0("variance of top cars' hp = ", req(var_top_cars_ret(mtcars, req(input$min_mpg))))
})
}
runApp(list(ui = ui, server = server_simple_null))
## this app _does_ work (doesn't crash)
## again, the output does not update if
## the slider goes too high
server_complex_null <- function(input, output) {
out <- reactiveValues(v = NA)
res <- reactive({
req(var_top_cars_ret(mtcars, req(input$min_mpg)))
})
observeEvent(req(input$min_mpg), {
out$v <- req(res())
})
output$variance <- renderText({
paste0("variance of top cars' hp = ", out$v)
})
}
runApp(list(ui = ui, server = server_complex_null))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment