Skip to content

Instantly share code, notes, and snippets.

@jcheng5
Last active January 6, 2022 21:27
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 jcheng5/368ed204f76fa9d130c0 to your computer and use it in GitHub Desktop.
Save jcheng5/368ed204f76fa9d130c0 to your computer and use it in GitHub Desktop.
Shiny without reactivity

Shiny without reactivity

In order to contrast Shiny's reactive programming model with the imperative programming techniques of traditional UI programming, here's a demonstration of what it looks like when you take Shiny's UI libraries and HTTP/websocket IO model, but remove the reactive programming aspects of it.

The original application code is here. In comparison, this strawman code is:

  • More verbose. The server function is more than twice as long.
  • Less efficient. During startup, the updateSelectedData() function runs twice, and updateClusters() and updatePlot() each run three times. Only one time each should be needed, which is what happens with the reactive version.
  • Less maintainable. When a particular input changes, the question of which update functions need to be called, and in what order, is critical for correctness. In non-trivial apps this is very difficult to get right without introducing bugs, especially as the features of the app evolve over time.
library(shiny)
# sendOutput is the imperative version of output$xxx <- renderYYY
sendOutput <- function(name, rendering) {
session <- getDefaultReactiveDomain()
value <- if (is.null(formals(rendering)))
rendering()
else
rendering(shinysession=session, name=name)
session$output[[name]] <- function() value
}
# Rename observeEvent to onChange, for readability
onChange <- observeEvent
# Remove the major reactive functions
observe <- reactive <- observeEvent <- eventReactive <- reactiveValues <- function(...) { stop("No cheating!") }
# Example app:
ui <- pageWithSidebar(
headerPanel('Iris k-means clustering'),
sidebarPanel(
selectInput('xcol', 'X Variable', names(iris)),
selectInput('ycol', 'Y Variable', names(iris),
selected=names(iris)[[2]]),
numericInput('clusters', 'Cluster count', 3,
min = 1, max = 9)
),
mainPanel(
plotOutput('plot1')
)
)
palette(c("#E41A1C", "#377EB8", "#4DAF4A", "#984EA3",
"#FF7F00", "#FFFF33", "#A65628", "#F781BF", "#999999"))
server <- function(input, output, session) {
selectedData <- NULL
clusters <- NULL
updateSelectedData <- function() {
message("Updating selected data")
# Combine the selected variables into a new data frame
selectedData <<- iris[, c(input$xcol, input$ycol)]
}
updateClusters <- function() {
message("Updating clusters")
clusters <<- kmeans(selectedData, input$clusters)
}
updatePlot <- function() {
message("Updating plot")
sendOutput("plot1", renderPlot({
par(mar = c(5.1, 4.1, 0, 1))
plot(selectedData,
col = clusters$cluster,
pch = 20, cex = 3)
points(clusters$centers, pch = 4, cex = 4, lwd = 4)
}))
}
onChange(input$xcol, {
message("xcol changed to: ", input$xcol)
updateSelectedData()
updateClusters()
updatePlot()
})
onChange(input$ycol, {
message("ycol changed to: ", input$ycol)
updateSelectedData()
updateClusters()
updatePlot()
})
onChange(input$clusters, {
message("clusters changed to: ", input$clusters)
updateClusters()
updatePlot()
})
}
shinyApp(ui, server)
@tyner
Copy link

tyner commented Jan 6, 2022

observeEvent is a wrapper around observe, so your onChange is still reliant on observe (from the shiny namespace).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment