Last active
February 27, 2021 03:45
-
-
Save jonocarroll/3fd80d80447476cdfec9b404fd08256d to your computer and use it in GitHub Desktop.
How should non-reactive functions handle errors to be compatible with shiny?
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
## 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