New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added skipFirst arg to observeEvent #1494
Conversation
IMO, having the user call I think that (2) definitely isn't the way to go, because it's too magical. |
One danger of using For example, here's how accessing the current observer from a reactive can get you in trouble, because the value of the reactive is cached: r <- reactive({
getCurrentObserver()$.label
})
observe(label = "observer 1", {
print(r())
})
observe(label = "observer 2", {
print(r())
})
shiny:::flushReact()
# [1] "observer 1"
# [1] "observer 1" |
After some discussion with Barbara, here are two things that we thought might be useful:
|
I just realized that skipFirst is going to have an issue with bookmarkable state. Or does observeEvent already have an issue with bookmarkable state? |
I was thinking the same for
|
@jcheng5 re |
@bborgesr observeEvent breaks it into two parts. "once" I'm assuming means the second part (handlerExpr) runs once. So you're saying "when this executes, whenever that may be, then destroy." For observe, that is always going to happen at the end of the current flushReact (unless you start it suspended) so "once" really means "execute this code after the current tick", in which case observe(once=TRUE) is a really weird way to express that. I don't think I'm explaining well, but do you get the gist of it? |
@jcheng5, I think I get it: since the Another question: do you think rv <- reactiveValues(btnExists = FALSE, justRemoved = NULL)
...
observeEvent(input[[btnID]], {
removeUI(selector = paste0("#", divID))
rv$btnExists <- FALSE
rv$justRemoved <- divID
}, skipFirst = TRUE, once = TRUE)
...
output$txt <- renderText({ paste("You just removed:", rv$justRemoved) }) Or, if we wanted to, it would be possible to adapt this into an justRemoved <- function() NULL
...
justRemoved <<- eventReactive(input[[btnID]], {
removeUI(selector = paste0("#", divID))
rv$btnExists <- FALSE
return(divID)
}, skipFirst = TRUE, once = TRUE)
...
output$txt <- renderText({ paste("You just removed:", justRemoved()) }) In this case, the |
eventReactive once doesn't make sense to me. If anything I think if you did that you'd want subsequent calls to the eventReactive to return the last cached value but never invalidate. But yeah doesn't seem like a good idea to pursue without a clear scenario. I do think we should resolve the bookmarking issue before deciding to merge this though. @wch any thoughts? |
re: bookmarking -- so if |
oh wait, you were talking about bookmarking +
For bookmarking to work with this, we just need the run = function() {
ctx <- .createContext()
.execCount <<- .execCount + 1L
ctx$run(.func)
} to: run = function() {
if (.initiliazed) {
ctx <- .createContext()
.execCount <<- .execCount + 1L
ctx$run(.func)
}
} What do you think? |
Talked to @wch and we don't see a problem with bookmarkable state. But we both agreed we should think a little more about alternatives to the param name |
#' \item \code{observe({ getCurrentObserver() }) } | ||
#' \item \code{observe({ (function(){ getCurrentObserver() })() )} } | ||
#' \item \code{observe({ isolate({ getCurrentObserver() }) }) } | ||
#' \item \code{observe({ reactive({ getCurrentObserver() }) }) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this will be confusing for most people. This is a bit clearer:
r <- reactive(getCurrentObserver()); observe(r())
#' \item \code{observe({ reactive({ getCurrentObserver() }) }) } | ||
#' } | ||
#' | ||
#' In (1), since you're outside of a reactive context, we've already |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd change this to simply, "In (1), since it is outside of a reactive context, \code{getCurrentObserver()} will return \code{NULL}."
#' it will always return \code{NULL}. There are a few subtleties, however. | ||
#' Consider the following five situations: | ||
#' | ||
#' \enumerate{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be useful to show, after each item, what gets returned, so that the reader doesn't have to scroll down to find out what the result is.
#' observer if we're inside an inner function's scope, or inside of an | ||
#' \code{isolate} or a \code{reactive} block? | ||
#' | ||
#' Before we can even asnwer that, there is an important distinction to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "asnwer". Although... I think perhaps this paragraph isn't necessary in these function docs. Maybe just something like this:
The expressions in
observe()
,reactive()
, andisolate()
create a new reactive context when they are executed. In contrast, regular function calls do not create new reactive contexts; they are not aware of reactivity.Normally,
getCurrentObserver()
looks only at the current reactive context and, if it was created by an observer, it returns that observer. IfgetCurrentObserver()
is called from another reactive context -- for example, one created by invoking areactive()
orisolate()
-- then, because the context was not created by an observer,getCurrentObserver()
will simply returnNULL
. If an observer calls a regular function that callsgetCurrentObserver()
, then it returns the observer, because the regular function has not created a new reactive context.
#' observeEvent(input$go, { | ||
#' print(paste("This will only be printed once; all", | ||
#' "subsequent button clicks won't do anything")) | ||
#' getCurrentObserver()$destroy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this particular example isn't a good use of getCurrentObserver()
, because the new once
option is more appropriate for this case.
#' \item \code{NULL} | ||
#' } | ||
#' | ||
#' Now, you may be wondering why \code{getCurrentObserver()} should't be able |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is also possible to look for the most recent observer in the reactive context "stack" by using
dig=TRUE
. The reactive context "stack" is analogous to a function call stack, except it tracks reactive contexts instead of function calls. With this option, whengetCurrentObserver()
is called from areactive
orisolate
, it will return the observer at the bottom of the stack. (Because observers are never called by other observers, there can only be one observer in the stack.)
@@ -1583,6 +1708,20 @@ maskReactiveContext <- function(expr) { | |||
#' @param ignoreNULL Whether the action should be triggered (or value | |||
#' calculated, in the case of \code{eventReactive}) when the input is | |||
#' \code{NULL}. See Details. | |||
#' @param skipFirst Whether the action should be run the first time the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When there's this much information, I'd suggest moving it outside of the documentation for the argument, to the main documentation section.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not the first time it's invalidated. It's when it's first created. How about ignoreInitial
? "When TRUE, the handler never runs when the observer is first created (even if the event expression returns a non-NULL value)."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like ignoreInitial
, especially because of keeping the same vocab as ignoreNULL
. But I feel like ignoreFirst
rolls off the tongue better. Is there a particular reason you're leaning towards "initial" instead of "first", @jcheng5?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I actually should have said ignoreInit, not initial. First and initial both sound like they could be "the first time the handler would run, skip/ignore it". But it really means "when the observer is first created/initialized, skip/ignore the handler whether it is otherwise supposed to run or not".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, perfect! Thanks @jcheng5! That is definitely the clearest way to explain this that any of us has come up with so far! This: "when the observer is first created/initialized, skip/ignore the handler whether it is otherwise supposed to run or not" makes the purpose of ignoreInit
so much clearer even in my own head!
@wch, @jcheng5: how are you feeling about param name |
I don't hate dig but call stacks are typically described as growing downward, not upwards. I don't know why but it's always been like this afaik. |
@jcheng5: how about
This makes more sense, if we like the |
How about recurse or recursive or inherit? |
I like |
…or `getCurrentObserver`
c938d86
to
55b90f8
Compare
As of now, this PR should be ready to merge. We went through lots of iterations but ended up agreeing on the following:
For now, we've agreed to table For possible future reference, here's the app that started this conversation with the correct working code: library(shiny)
ui <- fluidPage(
textInput("divID", "Enter div ID:", ""),
actionButton("isrt", "Create My Button"),
tags$div(id = "placeholder")
)
server <- function(input, output, session) {
rv <- reactiveValues(btnExists = FALSE)
# take a dependency on `isrt` button
observeEvent(input$isrt, {
# handle the case when user does not provide ID
divID <- if (input$divID == "") "id" else input$divID
btnID <- paste0(divID, "rmv")
# only create button if there is none
if (!rv$btnExists) {
insertUI(
selector = "#placeholder",
ui = tags$div(id = divID,
actionButton(btnID, "Remove this button"))
)
rv$btnExists <- TRUE
print("created")
# create a listener on the newly-created button that will
# remove it from the app when clicked
observeEvent(input[[btnID]], {
removeUI(selector = paste0("#", divID))
rv$btnExists <- FALSE
print("destroyed")
}, ignoreInit = TRUE, once = TRUE)
# otherwise, print a message to the console
} else {
message("The button has already been created!")
}
})
}
shinyApp(ui = ui, server = server) |
As we discussed (prompted by #1486), this adds a new argument (
skipFirst
) toobserveEvent
andeventReactive
that, whenTRUE
, will cause the action not to be triggered the very first time that the observer/reactive is run.This allows for, for example, observers for dynamically created action buttons not to be triggered when the button is created, and only when it is actually clicked.
Consider the following app:
This PR makes this app (that was completely broken before) work as it should, at least superficially: i.e. every time you click "Create my button", it will do so if there isn't a button there already. And each time you click on the newly-created button, it will remove itself. You can create and remove the button with the same id as many times as desired, and you can also change the id whenever you want.
BUT...
Unfortunately, that's not the end of the story. If you look closely at the app above, you'll see that it prints to the R console the word
created
whenever a button is created anddestroyed
whenever a button is destroyed. The good news is that every time a button is created or destroyed, the messages print, and that every time that one of the buttons (or the insert or remove button) is clicked, the appropriate message prints. This is why it appears from a user perspective that the app is working just fine. The bad news is that thedestroyed
message will be displayed x number of times, with x being the number of times that a button with that ID has been created before. So if you toggle between the "Create" and "Remove" buttons, from app startup, your console will look like:When really, it should be this:
I.e. if a button has already been destroyed, any observers that had been set up for it should disappear. This is, of course, possible, if we change the appropriate code to:
However, this should either 1) have a user-friendly wrapper / became (yet) another argument or 2) be a side effect of
removeUI()
-- the destruction of all observers or reactives that depend on that input.Is 2) really such a terrible idea? (I can see how this would get tricky if we're talking about an
observe
instead of anobserveEvent
-- or even anobserveEvent
that has something more complicated for the event thaninput[[btnID]]
)... But, it really feels like there should be an easier way of (at least optionally) getting rid of anobserveEvent
when the input it is responding to is removed...Thoughts?
cc @jcheng5 @wch