Meta counterparts to observeEvent() & eventReactive()

shinymeta currently does not provide meta-counterparts for eventReactive() and observeEvent(), but it’s possible to ‘hand-roll’ your own counterpart from existing building blocks (namely, metaReactive2(), metaObserve2(), and metaExpr()) Both of these functions are essentially a wrapper for a common reactive pattern where you want isolate() all reactive values except for one value/input. For example, if you want to

r <- eventReactive(input$x, {
  c(input$x, input$y)
})

is equivalent to:

r <- reactive({
  req(input$x)
  isolate({
    c(input$x, input$y)
  })
})

so, to create the meta-counterpart:

r <- metaReactive2({
  req(input$x)
  isolate(metaExpr({
    c(input$x, input$y)
  }))
})

Similarly, for observeEvent():

observeEvent(input$x, {
  message(input$x + input$y)
})

is equivalent to:

observe({
  req(input$x)
  isolate({
    message(input$x, input$y)
  })
})

so, to create the meta-counterpart:

o <- metaObserve2({
  req(input$x)
  isolate(metaExpr({
    message(input$x + input$y)
  }))
})

Warning: Do not attempt to use existing eventReactive()/observeEvent() by calling metaExpr() within their handler bodies. It won’t work with either one. eventReactive() won’t work because it caches its results, oblivious to whether it’s in normal mode or meta mode; and observeEvent() won’t work because (non-meta) observers don’t even have a way to return values, period.

Shiny modules

shinymeta is designed to work with Shiny modules, here’s an example. Modifying an existing Shiny app that uses modules may be more involved than simply capturing domain logic and marking reactive reads. For example, if you have one or more callModule() calls which create a (namespaced) output object(s), you may want to have that module function return the output object so you can expandChain() various meta-outputs from different modules at the same time (as done in the example app).

Shiny, tidyeval, and shinymeta

TL;DR: The same steps in the overview will work for a Shiny app that uses tidyeval, but it probably won’t produce the most readable code. To workaround that, if possible, try to avoid unquoting (i.e., !!/!!!) by using a functional interface that accepts character strings (instead of symbolic names).

Most tidyverse functions evaluate code expressions in a special context (e.g., they search for names within a data frame). That’s how dplyr knows, for example, to lookup names (e.g. cyl) and evaluate calls (e.g., mean(mpg)) within a context defined by mtcars:

library(dplyr)

# compute mean miles per gallon (mpg) by cylinder (cyl)
mtcars %>%
  group_by(cyl) %>%
  summarise(avg = mean(mpg))

[38;5;246m# A tibble: 3 × 2
[39m
    cyl   avg
  
[3m
[38;5;246m<dbl>
[39m
[23m 
[3m
[38;5;246m<dbl>
[39m
[23m

[38;5;250m1
[39m     4  26.7

[38;5;250m2
[39m     6  19.7

[38;5;250m3
[39m     8  15.1

This approach makes for an expressive interactive interface, but it also complicates things if we wish to pass variables into (i.e., program around) these functions (because they quote their arguments). For example, if you had a variable, named var, that represented another name with the column name of interest, dplyr thinks you’re looking for a column named var, not mpg:

var <- as.name("mpg")

mtcars %>%
  group_by(cyl) %>%
  summarise(avg = mean(var))
Warning: 
[1m
[22mThere were 3 warnings in `summarise()`.
The first warning was:

[1m
[22m
[36mℹ
[39m In argument: `avg = mean(var)`.

[36mℹ
[39m In group 1: `cyl = 4`.
Caused by warning in `mean.default()`:

[33m!
[39m argument is not numeric or logical: returning NA

[1m
[22m
[36mℹ
[39m Run `dplyr::last_dplyr_warnings()` to see the 2 remaining warnings.

[38;5;246m# A tibble: 3 × 2
[39m
    cyl   avg
  
[3m
[38;5;246m<dbl>
[39m
[23m 
[3m
[38;5;246m<dbl>
[39m
[23m

[38;5;250m1
[39m     4    
[31mNA
[39m

[38;5;250m2
[39m     6    
[31mNA
[39m

[38;5;250m3
[39m     8    
[31mNA
[39m

To workaround this problem, tidyverse functions allow you to unquote (i.e., replace a name with it’s value) via the !! operator. Just to demonstrate, if we unquote var, we’d get back the name (i.e., symbol) mpg.

rlang::expr(!!var)
mpg

That’s why this code gives us the desired result of average miles per gallon (mpg) per cylinder (cyl).

mtcars %>%
  group_by(cyl) %>%
  summarise(avg = mean(!!var))

[38;5;246m# A tibble: 3 × 2
[39m
    cyl   avg
  
[3m
[38;5;246m<dbl>
[39m
[23m 
[3m
[38;5;246m<dbl>
[39m
[23m

[38;5;250m1
[39m     4  26.7

[38;5;250m2
[39m     6  19.7

[38;5;250m3
[39m     8  15.1

Often times in a Shiny app we wish to pass an input value to a tidyverse function argument (as a variable). In most cases, that requires converting a string into a symbolic name, which can be done via as.symbol() or rlang::sym(). For example, here’s a Shiny app to compute the mean of different mtcars variables by cylinder (cyl).

library(shiny)
library(tidyverse)
ui <- fluidPage(
  selectInput("var", "Select a variable", names(mtcars)),
  verbatimTextOutput("out")
)
server <- function(input, output) {
  output$out <- renderPrint({
    var_sym <- sym(input$var)
    mtcars %>%
      group_by(cyl) %>%
      summarise(mean_mpg = mean(!!var_sym))
  })
}
shinyApp(ui, server)

Adding shinymeta support in this case is straight-forward. As with any other app, you’ll have to capture the domain logic (i.e., wrap renderPrint() with metaRender()), then mark reactive read ..().

server <- function(input, output) {
  output$out <- metaRender(renderPrint, {
    var_sym <- sym(..(input$var))
    mtcars %>%
      group_by(cyl) %>%
      summarise(mean_mpg = mean(!!var_sym))
  })
  observe(print(expandChain(output$out())))
}

This pattern also works when you need to convert a character vector of strings into a list of symbolic names (splice them into a function call using !!!).

ui <- fluidPage(
  selectInput("var", "Select variables", names(mtcars), multiple = TRUE),
  verbatimTextOutput("out")
)
server <- function(input, output) {
  output$out <- metaRender(renderPrint, {
    var_sym <- syms(..(input$var))
    select(mtcars, !!!var_sym)
  })
  observe(print(expandChain(output$out())))
}
shinyApp(ui, server)

In version v1.2.0, shiny introduced varSelectInput() essentially to remove the need to convert character string(s) into symbolic name(s). For example, in the app below, input$var already represents the symbolic name of interest, so you can do:

ui <- fluidPage(
  varSelectInput("var", "Select a variable", mtcars),
  verbatimTextOutput("out")
)
server <- function(input, output) {
  output$out <- renderPrint({
    mtcars %>%
      group_by(cyl) %>%
      summarise(mean_mpg = mean(!!input$var))
  })
}
shinyApp(ui, server)

As in the other examples, you can mark the reactive read with ..() (before unquoting with !!) and the code generation should “just work”. Technically speaking, this works because, when ..() encounters a symbolic name that it doesn’t recognize, it returns the code to generate the symbol instead of the bare symbol (i.e., it returns as.symbol("mpg") instead of mpg which makes the !! work in both normal and meta execution).

server <- function(input, output) {
  output$out <- metaRender(renderPrint, {
    mtcars %>%
      group_by(cyl) %>%
      summarise(mean_mpg = mean(!!..(input$var)))
  })
  observe(print(expandChain(output$out())))
}
shinyApp(ui, server)

In all the cases we’ve encountered thus far, the generated code is a bit different from how a human would write it. This last example produces code that looks like this:

mtcars %>%
  group_by(cyl) %>%
  summarise(mean_mpg = mean(!!as.symbol("mpg")))

But what you’d probably want your app to generate is this:

mtcars %>%
  group_by(cyl) %>%
  summarise(mean_mpg = mean(mpg))

At least currently, there is no great workaround for this problem other than to use alternative tidyverse functions that allow you to avoid unquoting by using character strings instead of symbolic names. Most dplyr functions provide this alternative through _at() variants. These variants allow you to write code like:

mtcars %>%
  group_by(cyl) %>%
  summarise_at("mpg", mean)

In this case, the implementation of the app is a lot simpler because we don’t have to worry about unquoting; plus, the code that the app generates looks a lot more like code a human would write:

ui <- fluidPage(
  selectInput("var", "Select a variable", names(mtcars)),
  verbatimTextOutput("out")
)
server <- function(input, output) {
  output$out <- metaRender(renderPrint, {
    mtcars %>%
      group_by(cyl) %>%
      summarise_at(..(input$var), mean)
  })
  observe(print(expandChain(output$out())))
}
shinyApp(ui, server)