如何创建可暂停/编辑的Shiny全局定时刷新模块
Solution: Reusable R6 Timer Module for Shiny (Rhino/Box)
Step 1: Create the R6 Timer Module
Replace your existing common_timer module with this R6-based implementation, which handles pause/resume and interval updates correctly:
box::use(shiny) box::use(R6[R6Class]) #' @export CommonTimer <- R6Class( "CommonTimer", public = list( #' @field trigger Reactive value that increments on each timer tick trigger = NULL, #' @description Initialize the timer #' @param interval Initial interval in milliseconds initialize = function(interval = 1500) { private$interval <- interval self$trigger <- shiny$reactiveVal(0) private$active <- FALSE }, #' @description Toggle timer on/off toggle_timer = function() { private$active <- !private$active if (private$active) { # Start new timer and observer to update trigger private$timer <- shiny$reactiveTimer(private$interval) private$observer <- shiny$observe({ private$timer() self$trigger(self$trigger() + 1) }) } else { # Clean up timer and observer when pausing if (!is.null(private$observer)) { private$observer$destroy() private$observer <- NULL } if (!is.null(private$timer)) { private$timer$destroy() private$timer <- NULL } } }, #' @description Update timer interval #' @param new_interval New interval in milliseconds set_interval = function(new_interval) { private$interval <- new_interval # Restart timer if active to apply new interval if (private$active) { self$toggle_timer() self$toggle_timer() } }, #' @description Check if timer is active #' @return Logical indicating active state is_active = function() { private$active } ), private = list( interval = NULL, active = FALSE, timer = NULL, observer = NULL ) ) #' @export # Singleton instance for app-wide use common_timer <- CommonTimer$new(interval = 1500)
Step 2: Update Data Call Module
Modify your call_data function to listen to the timer's reactive trigger:
box::use(shiny) #' @export live_data <- shiny$reactiveVal(NULL) #' @export date_interval <- shiny$reactiveValues( start_date = lubridate::as_datetime("2019-01-01"), end_date = lubridate::as_datetime("2019-01-28") ) box::use(lubridate) box::use(./common_timer[ common_timer ]) # Import the singleton timer box::use(./modules/helpers) box::use(./modules/cpp) #' @export call_data <- function(session, side_bar_options) { shiny$observeEvent(common_timer$trigger(), { currency <- shiny$isolate(side_bar_options$currency()) dt <- helpers$read_market_data( file = "./data/kucoin_btc-usdt-hourly.csv", symbols = currency, from = date_interval$start_date, to = date_interval$end_date ) live_data(dt) # Increment date range date_interval$start_date <- date_interval$start_date + lubridate$days(1) date_interval$end_date <- date_interval$end_date + lubridate$days(1) }) }
Step 3: Enhance Sidebar Module for Timer Control
Update the sidebar UI to include interval adjustment, and modify the server to interact with the timer:
Sidebar UI:
box::use(shiny) #' @export ui <- function(id, width = 2) { ns <- shiny$NS(id) shiny$sidebarPanel( shiny$tags$h4("Side bar"), shiny$selectInput( inputId = ns("select_currency"), label = "Currency:", choices = c("BTC/USDT", "XMR/BTC"), selected = "BTC/USDT" ), shiny$selectInput( inputId = ns("supplementary_plot"), label = "Supplementary Plot:", choices = c("RSI", "MACD", "Volume") ), # Add interval input shiny$numericInput( inputId = ns("interval"), label = "Refresh Interval (ms):", value = 1500, min = 500, max = 10000, step = 100 ), shiny$actionButton( inputId = ns("toggle_timer"), label = "Start" ), width = width ) }
Sidebar Server:
box::use(shiny) box::use(../logic/common_timer[ common_timer ]) # Import singleton timer #' @export server <- function(id, live_data) { shiny$moduleServer(id, \(input, output, session) { ns <- session$ns # Toggle timer on button click shiny$observeEvent(input$toggle_timer, { common_timer$toggle_timer() # Update button label based on state shiny$updateActionButton( session, "toggle_timer", label = if (common_timer$is_active()) "Pause" else "Start" ) }) # Update timer interval when input changes shiny$observeEvent(input$interval, { common_timer$set_interval(input$interval) }) supplementary_plot <- shiny$reactive({ input$supplementary_plot }) currency <- shiny$reactive({ input$select_currency }) return(list( currency = currency, supplementary_plot = supplementary_plot )) }) }
Key Fixes & Explanations
- R6 State Management: The R6 class encapsulates timer state (active/inactive, interval) and handles cleanup of observers/timers when pausing. This avoids stale reactiveTimer instances that can't be restarted.
- Reactive Trigger: The
triggerreactiveVal acts as a single source of truth for all components to listen to, ensuring synchronized refreshes. - Cleanup: When pausing, we explicitly destroy the reactiveTimer and observer to free resources, which was missing in your initial R6 attempt.
- Interval Updates: Changing the interval restarts the timer if it's active, ensuring the new interval takes effect immediately.
This implementation works seamlessly with Rhino and Box, providing a reusable, app-wide timer with full control over pause/resume and interval adjustments.
内容的提问来源于stack exchange,提问作者dereckmezquita
相关产品推荐
相关产品推荐

