You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Shiny仪表盘中响应式对象更新:健身活动追踪状态持久化问题

健身活动追踪仪表盘状态持久化问题

问题描述

我正在开发一个健身活动追踪仪表盘,采用Google Sheets作为后端存储。应用启动时会加载今日、昨日、前日的所有活动数据;初始界面展示当日可用活动的开关组件,开关状态从Google Sheets数据源预加载。

界面设有时间框架切换控件,可在今日、昨日、前日之间切换。目前存在的问题是:切换日期后,之前对某一天活动开关的修改,在切回该日期时会丢失。

我需要一个响应式对象,初始值为从Google Sheets下载的原始状态,每次点击开关时能更新对应日期对应活动的状态;最终仅在点击“上传”按钮时将修改同步至Google Sheets(该功能已实现)。

当前代码

library(tidyverse)
library(shiny)
library(shinyWidgets)

timeframes <- 
  tribble(
    ~date_name, ~date,
    "Today", today()
    , "Yesterday", today()-1 
    , "Day before yesterday", today()-2
  )

pre_assignment_vector <- 
  tibble::tribble(
    ~feature, ~date_name, ~value,
    "bike_into_coworking_space", "Day before yesterday",  FALSE,
    "bike_into_coworking_space", "Today",  FALSE,
    "bike_into_coworking_space", "Yesterday",   TRUE,
    
    "yoga", "Day before yesterday",  FALSE,
    "yoga", "Today",  FALSE,
    "yoga", "Yesterday",   TRUE
  ) %>% 
  mutate(value_name = paste0(feature, "_", snakecase::to_snake_case(date_name), "_status"),
         input_id_destination = paste0(feature, "_switch")
  )

assignment_vector <- 
  pre_assignment_vector %>% 
  filter(date_name == "Today")

map2(assignment_vector$value_name, assignment_vector$value, ~assign(.x, .y, envir = .GlobalEnv))

###################################################################

reactive_store_of_values_obj <- reactive(pre_assignment_vector)

###################################################################

# 日期切换时更新开关状态的函数
update_switches_to_date_selected <- function(reactive_store_of_values_obj, timeframe_selected) {
  
  values_to_update_to <- 
    reactive_store_of_values_obj %>% 
    filter(date_name == timeframe_selected)
  
  status_vals <- values_to_update_to
  
  map2(status_vals$input_id_destination, status_vals$value, ~updateSwitchInput(inputId = .x, value = as.logical(.y)))
  
}

###################################################################

# 开关变化时更新存储对象的函数
update_the_reactive_store_of_values_obj <- function(reactive_store_of_values_obj
                                                    , timeframe_selected = input$timeframe_selected
                                                    , yoga_switch = input$yoga_switch
                                                    , bike_into_coworking_space_switch = input$bike_into_coworking_space_switch) {
  
  new_values_from_inputs <- 
    tibble(yoga_switch = yoga_switch,
           bike_into_coworking_space_switch = bike_into_coworking_space_switch) %>% 
    gather(input_id_destination, new_value) %>% 
    mutate(date_name = timeframe_selected)
  
  reactive_store_of_values_obj %>% 
    left_join(new_values_from_inputs, by = join_by(date_name, input_id_destination)) %>% 
    mutate(value = coalesce(new_value, value)) %>% 
    select(-new_value)
  
}

###################################################################

ui <- fluidPage(
  
  mainPanel(
    
    fluidRow(
      
      h3("Timeframe"),
      
      sliderTextInput(inputId = "timeframe_selected",
                      label = NULL,
                      choices = rev(timeframes$date_name),
                      selected = timeframes$date_name[1])
    ), 
    
    fluidRow(
      
      column(width = 2, h4("Yoga")), 
      column(width = 1, prettySwitch(inputId = "yoga_switch", label = NULL, value = as.logical(yoga_today_status))),
      
    ), 
    
    fluidRow(
      
      column(width = 2, h4("Bike into Coworking Space")), 
      column(width = 1, prettySwitch(inputId = "bike_into_coworking_space_switch", label = NULL, value = as.logical(bike_into_coworking_space_today_status)))
      
    ) 
    
  )
)

server <- function(input, output) {
  
  observeEvent(ignoreInit = TRUE, input$timeframe_selected, {
    
    update_switches_to_date_selected(reactive_store_of_values_obj = reactive_store_of_values_obj(), timeframe_selected = input$timeframe_selected)
    
  })
  
  ###################################################################
  all_inputs_watch_list <- reactive(
    list(
      input$yoga_switch
      , input$bike_into_coworking_space_switch
    ))
  
  observeEvent(ignoreInit = TRUE, all_inputs_watch_list(), {
    
    print("switch used")
    
    reactive_store_of_values_obj <- reactive(update_the_reactive_store_of_values_obj(reactive_store_of_values_obj()))
    
  })
  
}

shinyApp(ui = ui, server = server)

解决方案

问题核心在于使用reactive()存储状态无法持久化修改——reactive()是被动响应式对象,每次重新赋值都会创建新的实例,旧状态会丢失。改用reactiveVal()可创建可主动更新的响应式存储,实现状态持久化。

修改后的完整代码

library(tidyverse)
library(shiny)
library(shinyWidgets)

timeframes <- 
  tribble(
    ~date_name, ~date,
    "Today", today()
    , "Yesterday", today()-1 
    , "Day before yesterday", today()-2
  )

pre_assignment_vector <- 
  tibble::tribble(
    ~feature, ~date_name, ~value,
    "bike_into_coworking_space", "Day before yesterday",  FALSE,
    "bike_into_coworking_space", "Today",  FALSE,
    "bike_into_coworking_space", "Yesterday",   TRUE,
    
    "yoga", "Day before yesterday",  FALSE,
    "yoga", "Today",  FALSE,
    "yoga", "Yesterday",   TRUE
  ) %>% 
  mutate(value_name = paste0(feature, "_", snakecase::to_snake_case(date_name), "_status"),
         input_id_destination = paste0(feature, "_switch")
  )

assignment_vector <- 
  pre_assignment_vector %>% 
  filter(date_name == "Today")

map2(assignment_vector$value_name, assignment_vector$value, ~assign(.x, .y, envir = .GlobalEnv))

###################################################################

# 改用reactiveVal创建可持久化的响应式存储
reactive_store_of_values_obj <- reactiveVal(pre_assignment_vector)

###################################################################

# 更新开关状态:从reactiveVal读取最新数据
update_switches_to_date_selected <- function(timeframe_selected) {
  current_data <- reactive_store_of_values_obj()
  
  values_to_update_to <- 
    current_data %>% 
    filter(date_name == timeframe_selected)
  
  map2(values_to_update_to$input_id_destination, values_to_update_to$value, ~updateSwitchInput(inputId = .x, value = as.logical(.y)))
}

###################################################################

# 更新存储对象:直接修改reactiveVal的值
update_the_reactive_store_of_values_obj <- function(timeframe_selected, yoga_switch, bike_switch) {
  current_data <- reactive_store_of_values_obj()
  
  new_values_from_inputs <- 
    tibble(
      input_id_destination = c("yoga_switch", "bike_into_coworking_space_switch"),
      new_value = c(yoga_switch, bike_switch)
    ) %>% 
    mutate(date_name = timeframe_selected)
  
  updated_data <- current_data %>% 
    left_join(new_values_from_inputs, by = join_by(date_name, input_id_destination)) %>% 
    mutate(value = coalesce(new_value, value)) %>% 
    select(-new_value)
  
  reactive_store_of_values_obj(updated_data)
}

###################################################################

ui <- fluidPage(
  
  mainPanel(
    
    fluidRow(
      
      h3("Timeframe"),
      
      sliderTextInput(inputId = "timeframe_selected",
                      label = NULL,
                      choices = rev(timeframes$date_name),
                      selected = timeframes$date_name[1])
    ), 
    
    fluidRow(
      
      column(width = 2, h4("Yoga")), 
      column(width = 1, prettySwitch(inputId = "yoga_switch", label = NULL, value = as.logical(yoga_today_status))),
      
    ), 
    
    fluidRow(
      
      column(width = 2, h4("Bike into Coworking Space")), 
      column(width = 1, prettySwitch(inputId = "bike_into_coworking_space_switch", label = NULL, value = as.logical(bike_into_coworking_space_today_status)))
      
    ),
    
    # 保留上传按钮位置(你已实现上传逻辑)
    fluidRow(
      column(width = 3, actionButton("upload_btn", "上传修改到Google Sheets"))
    )
    
  )
)

server <- function(input, output) {
  
  # 切换日期时更新开关状态
  observeEvent(ignoreInit = TRUE, input$timeframe_selected, {
    update_switches_to_date_selected(timeframe_selected = input$timeframe_selected)
  })
  
  # 监听开关变化,实时更新存储对象
  observeEvent(c(input$yoga_switch, input$bike_into_coworking_space_switch), {
    req(input$timeframe_selected)
    
    update_the_reactive_store_of_values_obj(
      timeframe_selected = input$timeframe_selected,
      yoga_switch = input$yoga_switch,
      bike_switch = input$bike_into_coworking_space_switch
    )
  })
  
  # 上传按钮逻辑(你已实现,示例留空)
  observeEvent(input$upload_btn, {
    # 从reactive_store_of_values_obj()获取最新数据,同步到Google Sheets
    showNotification("修改已上传", type = "success")
  })
  
}

shinyApp(ui = ui, server = server)

关键修改说明

  1. 状态存储:用reactiveVal()替代reactive(),允许主动调用reactive_store_of_values_obj(updated_data)更新存储内容,实现状态持久化。
  2. 函数逻辑调整:移除对全局input的依赖,通过参数传递输入值,避免作用域问题;更新函数直接修改reactiveVal的存储值。
  3. 监听简化:直接监听开关输入变化,无需额外创建监听列表,代码更简洁高效。
  4. 日期切换:从reactiveVal读取最新数据,确保切换日期时加载的是修改后的状态。

内容的提问来源于stack exchange,提问作者Julian Tagell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 06:48:10