Skip to content
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

Proposal for new feature: conditionalReactive #3298

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

stefanedwards
Copy link

@stefanedwards stefanedwards commented Feb 18, 2021

This PR proposes a new method that allows finer control over when an upstream, invalidated reactive should invalidate downstream reactives. Basically, it wraps

# in a server-function
value <- reactiveVal()
observe({
  oldvalue <- isolate(value())
  if (oldvalue != input$newvalue) {
    value(input$newvalue)
  }
})

into

value <- conditionalReactive(reactive(input$newvalue))

or, simply avoid triggering a downstream reactive if the input value has not change.

output$result <- renderX({
  value <- conditionalReactive(reactive(input$newvalue))
  # do stuff, but only if value has *changed* from previously, and not purely because something triggered `input$newvalue` to re-send an event with the same value
})

without the need of additional variables/reactives to keep track of new and old values.

Usage:

conditionalReactive(
  x,
  checkFun = NULL,
  fire.on.NULL = c("never", "change", "always"),
  fire.on.NA = c("never", "change", "always"),
  label = NULL
)

fire.on.NULL and fire.on.NA allows us to select different strategies if the new value is NULL or NA. In the first example (if (oldvalue != input$newvalue), if the new value is NULL or NA, the if-statement will error or not behave as expected (as the expression either returns a logical vector with length not-equal to 1, or simply NA). With these two arguments, the saves the user from hassle of checking for NAs and NULLs - while allowing them to decide e.g. whether a NULL should be considered different from the previous NULL.

checkFun allows us to fine-tune what is considered an inequality between the new and old value. One could decide that not only should the new value be different from the previous, but it should be different by a margin, before recalculating, i.e.

conditionalReactive(r, checkFunc = function(x,y) {abs(x-y) > 5})

Other scenarios:

Say a user must change 2 or more input values (e.g. x and y) before allowing invalidating downstreams reactives:

# without conditionalReactive
xy <- reactiveVal(c(0,0))
observe({
  newvalue <- c(input$x, input$y)
  if (all(newvalue != xy()) # xy is isolated by bindEvent
    xy(newvalue)
}) %>% bindEvent(input$x, input$y)

# with conditionalReactive
conditionalReactive(reactive(c(input$x, input$y)))

R6 classes, complex data structures, and side-effects
In instances where a recalculating can be computational consuming, shiny now offers bindCache. However, if the dependencies of the current reactive result are unclear (many, many inputs), listing these in bindCache is not attractive.
In cases of reference-like objects like R6, sharing the result among different users is a bad idea (i.e. don't cache if evaluation has side-effects).

Annoy your users
If you want to your users to click 3 times on an actionButton before moving on,

reactive({
  # wait for it...
  conditionalReactive(input$go, checkFunc = function(x,y) (x-y) >= 3)
  # go!
})

Avoids re-calculating old result when debounced
In DataTable, the user can select cells/rows/column, but if the input is combined with debounce, you can get situations where the input is re-sent with the same input, if a user selects then deselects the same item before the debounce fires. Additionally, for DataTable input, the input-reactive (e.g. input$dt_cells_selected) is NULL before the table is loaded, so any handling of the input has to deal with NULLs before considering whether the action is worth updating e.g. a render.

output$plot <- renderPlot({
  cells_selected <- conditionalReactive(reactive(input$dt_cells_selected))
  req(nrow(cells_selected) > 0)
})

Comparison to existing features of Shiny
Some of these scenarios might be solves through the use of req, validate, and bindCache, but by wrapping the conditionality into a method allows the code to be more readable, and the method takes care of some of the recurring obstacles of dealing with NULLs, NAs, or inputs with different lengths.

R CMD check
The pull request was checked with R version 4.0.3 on Windows 10 x64, and did not generate any additional errors (running R CMD check on master commit 60db1e02b03d8e6fb146c9bb1bbfbce269231add generated the following failure

-- FAILURE (test-built-files.R:26:3): shiny.css has been built -----------------
`new_css` (`actual`) not identical to `pkg_css` (`expected`).

lines(actual[[1]]) vs lines(expected[[1]])
- "pre.shiny-text-output:empty::before{content:\" \"}pre.shiny-text-output.noplaceholder:empty{margin:0;padding:0;border-width:0;height:0}pre.shiny-text-output{word-wrap:normal}.shiny-image-output img.shiny-scalable,.shiny-plot-output img.shiny-scalable{max-width:100%;max-height:100%}#shiny-disconnected-overlay{position:fixed;top:0;bottom:0;left:0;right:0;background-color:#999;opacity:0.5;overflow:hidden;z-index:99998;pointer-events:none}.table.shiny-table>thead>tr>th,.table.shiny-table>thead>tr>td,.table.shiny-table>tbody>tr>th,.table.shiny-table>tbody>tr>td,.table.shiny-table>tfoot>tr>th,.table.shiny-table>tfoot>tr>td{padding-right:12px;padding-left:12px}.shiny-table.spacing-xs>thead>tr>th,.shiny-table.spacing-xs>thead>tr>td,.shiny-table.spacing-xs>tbody>tr>th,.shiny-table.spacing-xs>tbody>tr>td,.shiny-table.spacing-xs>tfoot>tr>th,.shiny-table.spacing-xs>tfoot>tr>td{padding-top:3px;padding-bottom:3px}.shiny-table.spacing-s>thead>tr>th,.shiny-table.spacing-s>thead>tr>td,.shiny-table.spacing-s>tbody>tr>th,.shiny-table.spacing-s>tbody>tr>td,.shiny-table.spacing-s>tfoot>tr>th,.shiny-table.spacing-s>tfoot>tr>td{padding-top:5px;padding-bottom:5px}.shiny-table.spacing-m>thead>tr>th,.shiny-table.spacing-m>thead>tr>td,.shiny-table.spacing-m>tbody>tr>th,.shiny-table.spacing-m>tbody>tr>td,.shiny-table.spacing-m>tfoot>tr>th,.shiny-table.spacing-m>tfoot>tr>td{padding-top:8px;padding-bottom:8px}.shiny-table.spacing-l>thead>tr>th,.shiny-table.spacing-l>thead>tr>td,.shiny-table.spacing-l>tbody>tr>th,.shiny-table.spacing-l>tbody>tr>td,.shiny-table.spacing-l>tfoot>tr>th,.shiny-table.spacing-l>tfoot>tr>td{padding-top:10px;padding-bottom:10px}.shiny-table .NA{color:#909090}.shiny-output-error{color:red;white-space:pre-wrap}.shiny-output-error:before{content:'Error: ';font-weight:bold}.shiny-output-error-validation{color:#888}.shiny-output-error-validation:before{content:'';font-weight:inherit}@supports (-ms-ime-align: auto){.shiny-bound-output{transition:0}}.recalculating{opacity:0.3;transition:opacity 250ms ease 500ms}.slider-animate-container{text-align:right;margin-top:-9px}.slider-animate-button{opacity:0.5}.slider-animate-button .pause{display:none}.slider-animate-button.playing .pause{display:inline}.slider-animate-button .play{display:inline}.slider-animate-button.playing .play{display:none}.progress.shiny-file-input-progress{visibility:hidden}.progress.shiny-file-input-progress .progress-bar.bar-danger{transition:none}.shiny-input-container input[type=file]{overflow:hidden;max-width:100%}.shiny-progress-container{position:fixed;top:0px;width:100%;z-index:2000}.shiny-progress .progress{position:absolute;width:100%;top:0px;height:3px;margin:0px}.shiny-progress .bar{opacity:0.6;transition-duration:250ms}.shiny-progress .progress-text{position:absolute;right:10px;width:240px;background-color:#eef8ff;margin:0px;padding:2px 3px;opacity:0.85}.shiny-progress .progress-text .progress-message{padding:0px 3px;font-weight:bold;font-size:90%}.shiny-progress .progress-text .progress-detail{padding:0px 3px;font-size:80%}.shiny-progress-notification .progress{margin-bottom:5px;height:10px}.shiny-progress-notification .progress-text .progress-message{font-weight:bold;font-size:90%}.shiny-progress-notification .progress-text .progress-detail{font-size:80%}.shiny-label-null{display:none}.crosshair{cursor:crosshair}.grabbable{cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.grabbing{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.ns-resize{cursor:ns-resize}.ew-resize{cursor:ew-resize}.nesw-resize{cursor:nesw-resize}.nwse-resize{cursor:nwse-resize}.qt pre,.qt code{font-family:monospace !important}.qt5 .radio input[type=\"radio\"],.qt5 .checkbox input[type=\"checkbox\"]{margin-top:0px}.selectize-control{margin-bottom:10px}.shiny-frame{border:none}.shiny-flow-layout>div{display:inline-block;vertical-align:top;padding-right:12px;width:220px}.shiny-split-layout{width:100%;white-space:nowrap}.shiny-split-layout>div{display:inline-block;vertical-align:top;box-sizing:border-box;overflow:auto}.shiny-input-panel{padding:6px 8px;margin-top:6px;margin-bottom:6px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:2px}.shiny-input-checkboxgroup label~.shiny-options-group,.shiny-input-radiogroup label~.shiny-options-group{margin-top:-10px}.shiny-input-checkboxgroup.shiny-input-container-inline label~.shiny-options-group,.shiny-input-radiogroup.shiny-input-container-inline label~.shiny-options-group{margin-top:-1px}.shiny-input-container:not(.shiny-input-container-inline){width:300px;max-width:100%}.well .shiny-input-container{width:auto}.shiny-input-container>div>select:not(.selectized){width:100%}#shiny-notification-panel{position:fixed;bottom:0;right:0;background-color:rgba(0,0,0,0);padding:2px;width:250px;z-index:99999}.shiny-notification{background-color:#e8e8e8;color:#333;border:1px solid #ccc;border-radius:3px;opacity:0.85;padding:10px 8px 10px 10px;margin:2px}.shiny-notification-message{color:#31708f;background-color:#d9edf7;border:1px solid #bce8f1}.shiny-notification-warning{color:#8a6d3b;background-color:#fcf8e3;border:1px solid #faebcc}.shiny-notification-error{color:#a94442;background-color:#f2dede;border:1px solid #ebccd1}.shiny-notification-close{float:right;font-weight:bold;font-size:18px;bottom:9px;position:relative;padding-left:4px;color:#444;cursor:default}.shiny-notification-close:hover{color:#000}.shiny-notification-content-action a{color:#337ab7;text-decoration:underline;font-weight:bold}.shiny-file-input-active{box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.shiny-file-input-over{box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(76,174,76,0.6)}.datepicker table tbody tr td.disabled,.datepicker table tbody tr td.disabled:hover,.datepicker table tbody tr td span.disabled,.datepicker table tbody tr td span.disabled:hover{color:#aaa;cursor:not-allowed}.nav-hidden{display:none !important}"
  "pre.shiny-text-output:empty::before{content:\" \"}pre.shiny-text-output.noplaceholder:empty{margin:0;padding:0;border-width:0;height:0}pre.shiny-text-output{word-wrap:normal}.shiny-image-output img.shiny-scalable,.shiny-plot-output img.shiny-scalable{max-width:100%;max-height:100%}#shiny-disconnected-overlay{position:fixed;top:0;bottom:0;left:0;right:0;background-color:#999;opacity:0.5;overflow:hidden;z-index:99998;pointer-events:none}.table.shiny-table>thead>tr>th,.table.shiny-table>thead>tr>td,.table.shiny-table>tbody>tr>th,.table.shiny-table>tbody>tr>td,.table.shiny-table>tfoot>tr>th,.table.shiny-table>tfoot>tr>td{padding-right:12px;padding-left:12px}.shiny-table.spacing-xs>thead>tr>th,.shiny-table.spacing-xs>thead>tr>td,.shiny-table.spacing-xs>tbody>tr>th,.shiny-table.spacing-xs>tbody>tr>td,.shiny-table.spacing-xs>tfoot>tr>th,.shiny-table.spacing-xs>tfoot>tr>td{padding-top:3px;padding-bottom:3px}.shiny-table.spacing-s>thead>tr>th,.shiny-table.spacing-s>thead>tr>td,.shiny-table.spacing-s>tbody>tr>th,.shiny-table.spacing-s>tbody>tr>td,.shiny-table.spacing-s>tfoot>tr>th,.shiny-table.spacing-s>tfoot>tr>td{padding-top:5px;padding-bottom:5px}.shiny-table.spacing-m>thead>tr>th,.shiny-table.spacing-m>thead>tr>td,.shiny-table.spacing-m>tbody>tr>th,.shiny-table.spacing-m>tbody>tr>td,.shiny-table.spacing-m>tfoot>tr>th,.shiny-table.spacing-m>tfoot>tr>td{padding-top:8px;padding-bottom:8px}.shiny-table.spacing-l>thead>tr>th,.shiny-table.spacing-l>thead>tr>td,.shiny-table.spacing-l>tbody>tr>th,.shiny-table.spacing-l>tbody>tr>td,.shiny-table.spacing-l>tfoot>tr>th,.shiny-table.spacing-l>tfoot>tr>td{padding-top:10px;padding-bottom:10px}.shiny-table .NA{color:#909090}.shiny-output-error{color:red;white-space:pre-wrap}.shiny-output-error:before{content:'Error: ';font-weight:bold}.shiny-output-error-validation{color:#888}.shiny-output-error-validation:before{content:'';font-weight:inherit}@supports (-ms-ime-align: auto){.shiny-bound-output{transition:0}}.recalculating{opacity:0.3;transition:opacity 250ms ease 500ms}.slider-animate-container{text-align:right;margin-top:-9px}.slider-animate-button{opacity:0.5}.slider-animate-button .pause{display:none}.slider-animate-button.playing .pause{display:inline}.slider-animate-button .play{display:inline}.slider-animate-button.playing .play{display:none}.progress.shiny-file-input-progress{visibility:hidden}.progress.shiny-file-input-progress .progress-bar.bar-danger{transition:none}.shiny-input-container input[type=file]{overflow:hidden;max-width:100%}.shiny-progress-container{position:fixed;top:0px;width:100%;z-index:2000}.shiny-progress .progress{position:absolute;width:100%;top:0px;height:3px;margin:0px}.shiny-progress .bar{opacity:0.6;transition-duration:250ms}.shiny-progress .progress-text{position:absolute;right:10px;width:240px;background-color:#eef8ff;margin:0px;padding:2px 3px;opacity:0.85}.shiny-progress .progress-text .progress-message{padding:0px 3px;font-weight:bold;font-size:90%}.shiny-progress .progress-text .progress-detail{padding:0px 3px;font-size:80%}.shiny-progress-notification .progress{margin-bottom:5px;height:10px}.shiny-progress-notification .progress-text .progress-message{font-weight:bold;font-size:90%}.shiny-progress-notification .progress-text .progress-detail{font-size:80%}.shiny-label-null{display:none}.crosshair{cursor:crosshair}.grabbable{cursor:grab;cursor:-moz-grab;cursor:-webkit-grab}.grabbing{cursor:grabbing;cursor:-moz-grabbing;cursor:-webkit-grabbing}.ns-resize{cursor:ns-resize}.ew-resize{cursor:ew-resize}.nesw-resize{cursor:nesw-resize}.nwse-resize{cursor:nwse-resize}.qt pre,.qt code{font-family:monospace !important}.qt5 .radio input[type=\"radio\"],.qt5 .checkbox input[type=\"checkbox\"]{margin-top:0px}.selectize-control{margin-bottom:10px}.shiny-frame{border:none}.shiny-flow-layout>div{display:inline-block;vertical-align:top;padding-right:12px;width:220px}.shiny-split-layout{width:100%;white-space:nowrap}.shiny-split-layout>div{display:inline-block;vertical-align:top;box-sizing:border-box;overflow:auto}.shiny-input-panel{padding:6px 8px;margin-top:6px;margin-bottom:6px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:2px}.shiny-input-checkboxgroup label~.shiny-options-group,.shiny-input-radiogroup label~.shiny-options-group{margin-top:-10px}.shiny-input-checkboxgroup.shiny-input-container-inline label~.shiny-options-group,.shiny-input-radiogroup.shiny-input-container-inline label~.shiny-options-group{margin-top:-1px}.shiny-input-container:not(.shiny-input-container-inline){width:300px;max-width:100%}.well .shiny-input-container{width:auto}.shiny-input-container>div>select:not(.selectized){width:100%}#shiny-notification-panel{position:fixed;bottom:0;right:0;background-color:rgba(0,0,0,0);padding:2px;width:250px;z-index:99999}.shiny-notification{background-color:#e8e8e8;color:#333;border:1px solid #ccc;border-radius:3px;opacity:0.85;padding:10px 8px 10px 10px;margin:2px}.shiny-notification-message{color:#31708f;background-color:#d9edf7;border:1px solid #bce8f1}.shiny-notification-warning{color:#8a6d3b;background-color:#fcf8e3;border:1px solid #faebcc}.shiny-notification-error{color:#a94442;background-color:#f2dede;border:1px solid #ebccd1}.shiny-notification-close{float:right;font-weight:bold;font-size:18px;bottom:9px;position:relative;padding-left:4px;color:#444;cursor:default}.shiny-notification-close:hover{color:#000}.shiny-notification-content-action a{color:#337ab7;text-decoration:underline;font-weight:bold}.shiny-file-input-active{box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(102,175,233,0.6)}.shiny-file-input-over{box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(76,174,76,0.6)}.datepicker table tbody tr td.disabled,.datepicker table tbody tr td.disabled:hover,.datepicker table tbody tr td span.disabled,.datepicker table tbody tr td span.disabled:hover{color:#aaa;cursor:not-allowed}.nav-hidden{display:none !important}\r"
  ""

Passes R CMD check.
@CLAassistant
Copy link

CLAassistant commented Feb 18, 2021

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@wch
Copy link
Collaborator

wch commented Feb 25, 2021

Hi, thanks for taking the time to put this together. We have been gradually collecting examples of useful higher-order reactives that could be useful enough for users to merit a new function in Shiny. We are planning revisit the topic at some point in the future and decide what to do with these ideas. In other words, we won't merge this now, but we will take a look again in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants