Skip to content

Commit 89026ee

Browse files
committed
added convenience function: getCurrentObserver. The dig param is problematic (if it's FALSE -- this does not work for observeEvent, only observe)
1 parent e0868ba commit 89026ee

File tree

4 files changed

+251
-0
lines changed

4 files changed

+251
-0
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export(fluidPage)
8484
export(fluidRow)
8585
export(formatStackTrace)
8686
export(freezeReactiveValue)
87+
export(getCurrentObserver)
8788
export(getDefaultReactiveDomain)
8889
export(getShinyOption)
8990
export(h1)

R/reactives.R

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,10 @@ execCount <- function(x) {
703703

704704
# Observer ------------------------------------------------------------------
705705

706+
# The initial value of "current observer" is NULL (and will always be NULL,
707+
# except when within the scope of the observe or observeEvent)
708+
.globals$currentObserver <- NULL
709+
706710
Observer <- R6Class(
707711
'Observer',
708712
portable = FALSE,
@@ -814,6 +818,8 @@ registerDebugHook("observerFunc", environment(), label)
814818
run = function() {
815819
ctx <- .createContext()
816820
.execCount <<- .execCount + 1L
821+
.globals$currentObserver <- self
822+
on.exit(.globals$currentObserver <- NULL) # On exit, set it back to NULL
817823
ctx$run(.func)
818824
},
819825
onInvalidate = function(callback) {
@@ -904,6 +910,125 @@ registerDebugHook("observerFunc", environment(), label)
904910
)
905911
)
906912

913+
#' Return the current observer
914+
#'
915+
#' This function is useful when you want to access an observer's methods or
916+
#' variables directly. For example, you may have logic that destroys or
917+
#' suspends the observer (from within its own scope) on some condition.
918+
#'
919+
#' This function works by returning the observer that is currently being run
920+
#' when \code{getCurrentObserver()} is called. If there is no observer being
921+
#' run (for example, if you called it from outside of a reactive context),
922+
#' it will always return \code{NULL}. There are a few subtleties, however.
923+
#' Consider the following five situations:
924+
#'
925+
#' \enumerate{
926+
#' \item \code{getCurrentObserver() #outside of a reactive context}
927+
#' \item \code{observe({ getCurrentObserver() }) }
928+
#' \item \code{observe({ (function(){ getCurrentObserver() })() )} }
929+
#' \item \code{observe({ isolate({ getCurrentObserver() }) }) }
930+
#' \item \code{observe({ reactive({ getCurrentObserver() }) }) }
931+
#' }
932+
#'
933+
#' In (1), since you're outside of a reactive context, we've already
934+
#' established that \code{getCurrentObserver()} will return \code{NULL}.
935+
#' In (2), we have the "vanilla" case, in which \code{getCurrentObserver()}
936+
#' is called directly from within the body of the \code{observe} call.
937+
#' This returns that observer. So far, so good. The problem comes with
938+
#' the last three cases -- should we be able to "retrieve" the outer
939+
#' observer if we're inside an inner function's scope, or inside of an
940+
#' \code{isolate} or a \code{reactive} block?
941+
#'
942+
#' Before we can even asnwer that, there is an important distinction to
943+
#' be made here: are function calls, \code{reactive} calls and
944+
#' \code{isolate} blocks the same \emph{type} of thing? As far as Shiny
945+
#' is concerned, the answer is no. Shiny-specific things (like observers,
946+
#' reactives and code inside of an \code{isolate} chunk) exist in what we
947+
#' call reactive contexts. Each run of an observer or a reactive is
948+
#' associated with a particular reactive context. But regular functions
949+
#' have no relation to reactive contexts. So, while calling a regular
950+
#' function inside of an observer does not change the reactive context,
951+
#' calling a \code{reactive} or \code{isolate} certainly does.
952+
#'
953+
#' With this distinction in mind, we can refine our definition of
954+
#' \code{getCurrentObserver()} as follows: it returns the observer (if any)
955+
#' that is currently running, as long as it is called from within the
956+
#' same reactive context that was created when the observer started
957+
#' running. If the reactive context changed (most likely because of a
958+
#' call to \code{reactive} or \code{isolate}), \code{getCurrentObserver}
959+
#' will return \code{NULL}. (There is another common way that the reactive
960+
#' context can change inside an observer, which is if there is a second,
961+
#' nested observer. In this case, \code{getCurrentObserver()} will return
962+
#' the second, nested observer, since that is the one that is actually
963+
#' running at that time.)
964+
#'
965+
#' So to recap, here's the return value for each of the five situations:
966+
#' \enumerate{
967+
#' \item \code{NULL}
968+
#' \item the observer
969+
#' \item the observer
970+
#' \item \code{NULL}
971+
#' \item \code{NULL}
972+
#' }
973+
#'
974+
#' Now, you may be wondering why \code{getCurrentObserver()} should't be able
975+
#' to get the running observer even if the reactive context changes. This isn't
976+
#' technically impossible. In fact, if you want this behavior for some reason,
977+
#' you can set the argument \code{dig} to be \code{TRUE}, so that the function
978+
#' will "dig" through the reactive contexts until it retrieves the one for the
979+
#' observer and returns the observer.
980+
#'
981+
#' So, with \code{dig = TRUE}, here's the return value for each of the five
982+
#' situations:
983+
#' \enumerate{
984+
#' \item \code{NULL}
985+
#' \item the observer
986+
#' \item the observer
987+
#' \item the observer
988+
#' \item the observer
989+
#' }
990+
#'
991+
#' The reason that this is not the default (or even encouraged) is because
992+
#' things can get messy quickly when you cross reactive contexts at will.
993+
#' For example, the return value of a \code{reactive} call is cached and that
994+
#' reactive is not re-run unless its reactive dependencies change. If that
995+
#' reactive has a call to \code{getCurrentObserver()}, this can produce
996+
#' undesirable and unintuitive results.
997+
#'
998+
#' @param dig If \code{FALSE} (default), \code{getCurrentObserver} will only
999+
#' return the observer if it's invoked directly from within the observer's
1000+
#' body or from a regular function. If \code{TRUE}, it will always return
1001+
#' the observer (if it exists on the stack), even if it's invoked from
1002+
#' within a \code{reactive} or an \code{isolate} scope. See below for more
1003+
#' information.
1004+
#'
1005+
#' @return The observer (created with a call to either \code{observe} or to
1006+
#' \code{observeEvent}) that is currently running.
1007+
#'
1008+
#' @seealso \code{\link{observe}}
1009+
#'
1010+
#' @examples
1011+
#' ## Only run examples in interactive R sessions
1012+
#' if (interactive()) {
1013+
#' shinyApp(
1014+
#' ui = basicPage( actionButton("go", "Go")),
1015+
#' server = function(input, output, session) {
1016+
#' observeEvent(input$go, {
1017+
#' print(paste("This will only be printed once; all",
1018+
#' "subsequent button clicks won't do anything"))
1019+
#' getCurrentObserver()$destroy()
1020+
#' })
1021+
#' }
1022+
#' )
1023+
#' }
1024+
#' @export
1025+
getCurrentObserver <- function(dig = FALSE) {
1026+
o <- .globals$currentObserver
1027+
ctx <- getCurrentContext()
1028+
if (!dig && !is.null(o) && ctx$id != o$.ctx$id) o <- NULL
1029+
o
1030+
}
1031+
9071032
#' Create a reactive observer
9081033
#'
9091034
#' Creates an observer from the given expression.

inst/staticdocs/index.r

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ sd_section("Reactive constructs",
124124
"makeReactiveBinding",
125125
"observe",
126126
"observeEvent",
127+
"getCurrentObserver",
127128
"reactive",
128129
"reactiveFileReader",
129130
"reactivePoll",

man/getCurrentObserver.Rd

Lines changed: 124 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)