vignettes/special-topics.Rmd
special-topics.Rmd
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:
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:
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.
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).
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:
But what you’d probably want your app to generate is this:
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)