从 DT::renderDT 调用时,R shiny 反应值不会重新计算

R shiny reactive value does not recalculate when called from DT::renderDT

我闪亮的应用程序中的反应值在第一次计算后从 DT::renderDT 函数内部调用时不会重新计算。

这是我的代码:

#-------------------------------------------------------------------------------------------------
# ENVIRONMENT & PACKAGES
#-------------------------------------------------------------------------------------------------

# Set working directory
setwd('C:/Users/username/OneDrive/Desktop/Coding projects/Paleo Diet Planner')

# Clear workspace
rm(list = ls())

# Package/library list
pckgs <- c('shiny','shinydashboard','reactlog','DT','dplyr','stringr','mgsub')

# Install and load libraries
for(pckg in pckgs)
{
  if(!(pckg %in% rownames(installed.packages()))) install.packages(pckg)
  if(!(pckg %in% (.packages()))) library(pckg, character.only = TRUE)
}

# Remove unnecessary variables
rm(pckg,pckgs)

#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# DATA
#-------------------------------------------------------------------------------------------------

# Static Data
Recipe_Inv <- readRDS('./Data/Recipe_Inv.rds')

# Get unique tags
Recipe_Tags <- Recipe_Inv$Tags %>% stringr::str_split(., ';', simplify = T) %>%
  trimws %>% as.vector %>% unique %>% magrittr::extract(. != '') %>% magrittr::extract(order(.))

# Utility variables
Debug_Flag <- F
Log_Flag <- T
LogFile <- c()
LogFile_Path <- as.character(Sys.time()) %>% mgsub(., c('-', ' ', ':'), c('', '_', '-')) %>% paste0('./Logs/',.,'.txt')

#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# UTILITY FUNCTIONS
#-------------------------------------------------------------------------------------------------

# Function for checking if the tags chosen for filtering are present in the tag string, i.e.
# string with tags separated by semi-colon
Filter_Tags <- function(Tags, Tag_Crit){
  # Debug and log
  # if(Log_Flag) print('Utility_Function -> Filter_Tags')
  if(Debug_Flag) browser()
  
  #Extract tags from the tag string as character vector
  Tag_Ls <- trimws(stringr::str_split(Tags, ';')[[1]])
  
  # Check if the intersection (common elements) of the tag string and filter tags vector have the same length
  # Equivalent to all filter tags being present in the tag string
  length(intersect(Tag_Ls, Tag_Crit)) == length(Tag_Crit)
}

# Function to filter out, rearrange and order the tags when select drop-down field is used
ReArrange_Tags <- function(Tags, Tag_Crit){
  # Log
  # if(Log_Flag) print('Utility_Function - > ReArrange_Tags')
  
  if(is.null(Tag_Crit)){
    # If the select drop-down list is empty, leave tag string (tags delimited by semi-colon) as-is
    Tags
  }else{
    # Debug
    if(Debug_Flag) browser()
    
    # Get the tags in the tag string that were chosen in the select drop-down field
    # and order them aplhabetically
    trimws(stringr::str_split(Tags, ';')[[1]]) %>% 
      intersect(., Tag_Crit) %>% magrittr::extract(order(.)) %>%
      paste0(., collapse = ';')
  }
}

# Function for logging events in the apps in terms of UI element usage and server activity
Log_App_Activity <- function(print_txt){
  print(print_txt)
  LogFile <<- c(LogFile,print_txt)
  write.table(LogFile, LogFile_Path, sep = '\n', col.names = F, row.names = F)
}

#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# USER INTERFACE
#-------------------------------------------------------------------------------------------------

#### Separate components #### 

# Dashboard Header
db_header <- shinydashboard::dashboardHeader(
  # Dashboard title
  title = 'Paleo Diet Planner'
)

# Dashboard Sidebar
db_Sidebar <- shinydashboard::dashboardSidebar(
  #### Sidebar settings ####
  # ID of the dashboard sidebar object
  id = 'InSidebar_Menu',
  
  # Width setting
  width = 350,
  
  #### UI elements ####
  #
  shiny::selectInput(inputId = 'InSelect_RecipeTags',
                     label = 'Select Recipe Categories',
                     choices = Recipe_Tags, multiple = T),
  
  #
  shinydashboard::menuItem(text = 'Recipe List', tabName = 'tbRecipeLs',
                           icon = shiny::icon('th-list')),
  
  #
  shinydashboard::menuItem(text = 'Recipe View', tabName = 'tbRecipeView',
                           icon = shiny::icon('readme'))
)

# Dashboard Body
db_Body <- shinydashboard::dashboardBody(
  
  #
  shinydashboard::tabItems(
    #
    shinydashboard::tabItem(
      #
      tabName = 'tbRecipeLs',
      #
      DT::DTOutput(outputId = 'OutDT_RecipeList')
    ),
    
    #
    shinydashboard::tabItem(
      #
      tabName = 'tbRecipeView',
      #
      shiny::uiOutput('OutUI_RecipeURL'),
      #
      shiny::htmlOutput("OutUI_RecipeWebsite")
    )
  )
)

#### Main UI #### 
Main_UI <- dashboardPage(db_header, db_Sidebar, db_Body)

#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# SERVER
#-------------------------------------------------------------------------------------------------

# Define server logic
server <- function(input, output){
  
  # Filtered Recipe Inventory based on the select drop-down field 'InSelect_RecipeTags'
  Recipe_Inv_Flt <- shiny::reactive({
    # Debug and log
    if(Log_Flag) Log_App_Activity('shiny::reactive -> Recipe_Inv_Flt')
    if(Debug_Flag) browser()
    
    # Check if the select drop-down field 'InSelect_RecipeTags' has been used and filter appropriately
    if(is.null(input$InSelect_Recipe_Tags)){
      Recipe_Inv
    }else{
      Recipe_Inv %>% dplyr::rowwise() %>%
        dplyr::filter(Filter_Tags(Tags, input$InSelect_RecipeTags)) %>%
        dplyr::ungroup()
    }
  })
  
  # Data Table displaying the reactive values Recipe_Inv_Flt() with appropriate tags displayed
  # in the tag column, based on the tags selected in the select drop-down field 'InSelect_RecipeTags'
  output$OutDT_RecipeList <- DT::renderDT({
    # Debug and log
    if(Log_Flag) Log_App_Activity('DT::renderDT -> OutDT_RecipeList')
    if(Debug_Flag) browser()
    
    # 
    Recipe_Inv_Flt() %>% dplyr::select(Tags, Recipe_Nm) %>% dplyr::rowwise() %>%
      dplyr::mutate(Tags = ReArrange_Tags(Tags, input$InSelect_RecipeTags)) %>%
      dplyr::ungroup() %>% dplyr::arrange(Tags)
  })
  
  #
  output$OutUI_RecipeURL <- shiny::renderUI({
    # Debug and log
    if(Log_Flag) Log_App_Activity('shiny::renderUI -> OutUI_RecipeURL')
    if(Debug_Flag) browser(text = 'shiny::renderUI -> OutUI_RecipeURL')
    
    #
    choices <- Recipe_Inv_Flt() %>% .$Recipe_Nm %>% magrittr::extract(input$OutDT_RecipeList_rows_selected)
    
    #
    shiny::selectInput(inputId = 'InSelect_RecipeURL',
                       label = 'Select recipe to display',
                       choices = choices, multiple = F)
  })
  
  #
  output$OutUI_RecipeWebsite <- shiny::renderUI({
    #Debug and log
    if(Log_Flag) Log_App_Activity('shiny::renderUI -> OutUI_RecipeWebsite')
    if(Debug_Flag) browser(text = 'shiny::renderUI -> OutUI_RecipeWebsite')
    
    #
    if(!is.null(input$InSelect_RecipeURL)){
      Recipe_URL <- Recipe_Inv %>% dplyr::filter(Recipe_Nm == input$InSelect_RecipeURL) %>% .$Recipe_URL
    }else{
      Recipe_URL <- NA
    }
    
    #
    shiny::tags$iframe(src = Recipe_URL, width = "100%", height = 800)
  })
  
  # Log for UI elements
  shiny::observeEvent(input$InSelect_RecipeTags,{
    browser()
    if(Log_Flag) Log_App_Activity('shiny::selectInput -> InSelect_RecipeTags')
  })
  
  shiny::observeEvent(input$InSidebar_Menu,{
    browser()
    if(Log_Flag){
      switch(input$InSidebar_Menu,
             'tbRecipeLs' = Log_App_Activity('shinydashboard::menuIte -> tbRecipeLs'),
             'tbRecipeView' = Log_App_Activity('shinydashboard::menuIte -> tbRecipeView'))
    }
  })
  
  shiny::observeEvent(input$InSelect_RecipeURL,{
    browser()
    if(Log_Flag) Log_App_Activity('shiny::selectInput -> InSelect_RecipeURL')
  })
}

#-------------------------------------------------------------------------------------------------

#-------------------------------------------------------------------------------------------------
# RUN APPLICATION
#-------------------------------------------------------------------------------------------------

shiny::shinyApp(ui = Main_UI, server = server)

#-------------------------------------------------------------------------------------------------

这是 sessionInfo() 的输出:

R version 3.6.2 (2019-12-12)
Platform: x86_64-w64-mingw32/x64 (64-bit)
Running under: Windows 10 x64 (build 19042)

Matrix products: default

locale:
[1] LC_COLLATE=Polish_Poland.1250  LC_CTYPE=Polish_Poland.1250    LC_MONETARY=Polish_Poland.1250 LC_NUMERIC=C                  
[5] LC_TIME=Polish_Poland.1250    

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

other attached packages:
[1] mgsub_1.7.3          stringr_1.4.0        dplyr_1.0.2          DT_0.14              reactlog_1.1.0       shinydashboard_0.7.1
[7] shiny_1.4.0         

loaded via a namespace (and not attached):
 [1] Rcpp_1.0.5        rstudioapi_0.13   magrittr_1.5      tidyselect_1.1.0  xtable_1.8-4      R6_2.4.1          rlang_0.4.11      fastmap_1.0.1    
 [9] tools_3.6.2       ellipsis_0.3.2    crosstalk_1.1.0.1 htmltools_0.4.0   yaml_2.2.1        digest_0.6.25     tibble_2.1.3      lifecycle_1.0.0  
[17] crayon_1.3.4      purrr_0.3.3       later_1.0.0       htmlwidgets_1.5.1 vctrs_0.3.4       promises_1.1.0    glue_1.4.1        mime_0.9         
[25] stringi_1.4.6     compiler_3.6.2    pillar_1.4.3      generics_0.0.2    jsonlite_1.7.0    httpuv_1.5.2      pkgconfig_2.0.3 

Recipe_Inv 变量具有以下(20 个示例行)结构:

Recipe_Nm Recipe_URL Tags
Apple Butter https://paleoleap.com/apple-butter/ PALEO DESSERTS;SWEETS AND SNACKS;COOKING: FAST PREP;COOKING: SLOW-COOKER;PALEO AUTOIMMUNE-FRIENDLY RECIPES;DIET: EGG-FREE;DIET: NUT-FREE;DIET: VEGETARIAN;PALEO BUDGET-FRIENDLY RECIPES;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Baked Eggs With Asparagus and Leeks https://paleoleap.com/baked-eggs-asparagus-leeks/ PALEO PORK RECIPES;PALEO EGG RECIPES;PALEO LOW-CARB RECIPES;PALEO BREAKFAST RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;DIET: NUT-FREE;PALEO BUDGET-FRIENDLY RECIPES
Beef Pho https://paleoleap.com/beef-pho/ PALEO BEEF AND RED MEAT RECIPES;PALEO SOUP RECIPES;PALEO BREAKFAST RECIPES;PALEO LUNCH RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;PALEO AUTOIMMUNE-FRIENDLY RECIPES;DIET: EGG-FREE;DIET: LOW-FODMAP;DIET: NUT-FREE
Chicken Cashew Casserole https://paleoleap.com/chicken-cashew-casserole/ PALEO CHICKEN AND POULTRY RECIPES;PALEO LOW-CARB RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;DIET: EGG-FREE
Chicken With Garlic-Roasted Sweet Potatoes https://paleoleap.com/chicken-garlic-roasted-sweet-potatoes/ PALEO CHICKEN AND POULTRY RECIPES;PALEO DINNER RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;DIET: EGG-FREE;DIET: NUT-FREE;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Crab-Stuffed Deviled Eggs With Tarragon https://paleoleap.com/crab-stuffed-deviled-eggs-tarragon/ PALEO FISH AND SEAFOOD RECIPES;PALEO EGG RECIPES;PALEO LOW-CARB RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;DIET: LOW-FODMAP;DIET: NUT-FREE;GOOD FOR LEFTOVERS
Elk Shepherd’s Pie https://paleoleap.com/elk-shepherd-pie/ PALEO BEEF AND RED MEAT RECIPES;PALEO DINNER RECIPES;DIET: EGG-FREE;DIET: NUT-FREE;PALEO BUDGET-FRIENDLY RECIPES;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Fresh Fruit And Kale Salad https://paleoleap.com/fresh-fruit-and-kale-salad/ PALEO SALAD RECIPES;PALEO BREAKFAST RECIPES;COOKING: FAST PREP;DIET: EGG-FREE;DIET: VEGETARIAN
Garlic-Roasted Cherry Tomatoes https://paleoleap.com/garlic-roasted-cherry-tomatoes/ PALEO SIDES;VEGGIES AND APPETIZERS;PALEO LOW-CARB RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;DIET: EGG-FREE;DIET: NUT-FREE;DIET: VEGETARIAN;PALEO BUDGET-FRIENDLY RECIPES
Ginger Carrot Soup https://paleoleap.com/ginger-carrot-soup/ PALEO SOUP RECIPES;PALEO LOW-CARB RECIPES;PALEO BREAKFAST RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;PALEO AUTOIMMUNE-FRIENDLY RECIPES;DIET: EGG-FREE;DIET: NUT-FREE;DIET: VEGETARIAN;PALEO BUDGET-FRIENDLY RECIPES;GOOD FOR LEFTOVERS
Grilled Pineapple Chicken https://paleoleap.com/grilled-pineapple-chicken/ PALEO CHICKEN AND POULTRY RECIPES;PALEO DINNER RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;COOKING: GRILL;DIET: EGG-FREE;DIET: NUT-FREE;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Italian-Style Fish Bowls https://paleoleap.com/italian-style-fish-bowl/ PALEO FISH AND SEAFOOD RECIPES;PALEO LOW-CARB RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;DIET: EGG-FREE;DIET: NUT-FREE
Keto Slow Cooker Chicken Soup https://paleoleap.com/slow-cooker-chicken-soup/ PALEO CHICKEN AND POULTRY RECIPES;PALEO SOUP RECIPES;PALEO LOW-CARB RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;COOKING: SLOW-COOKER;DIET: EGG-FREE;DIET: NUT-FREE;PALEO BUDGET-FRIENDLY RECIPES;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Maple-Barbecue Ribs https://paleoleap.com/maple-barbecue-ribs/ PALEO PORK RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;DIET: EGG-FREE;DIET: NUT-FREE;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Paleo Garlic Shrimp With Zucchini Noodles https://paleoleap.com/garlic-shrimp-with-zucchini-noodle/ PALEO FISH AND SEAFOOD RECIPES;PALEO LOW-CARB RECIPES;PALEO LUNCH RECIPES;PALEO DINNER RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;PALEO AUTOIMMUNE-FRIENDLY RECIPES;DIET: EGG-FREE;PALEO KID-FRIENDLY RECIPES
Pumpkin Cookies https://paleoleap.com/pumpkin-cookies/ PALEO DESSERTS;SWEETS AND SNACKS;COOKING: FAST COOK;COOKING: FAST PREP;DIET: EGG-FREE;DIET: VEGETARIAN;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Sautéed Chicken And Cabbage https://paleoleap.com/sauteed-chicken-cabbage/ PALEO CHICKEN AND POULTRY RECIPES;PALEO LOW-CARB RECIPES;PALEO LUNCH RECIPES;PALEO DINNER RECIPES;COOKING: FAST PREP;DIET: EGG-FREE;DIET: NUT-FREE;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES
Slow Cooker Butterkin And Nuts https://paleoleap.com/slow-cooker-butterkin-and-nuts/ PALEO SIDES;VEGGIES AND APPETIZERS;COOKING: FAST PREP;COOKING: SLOW-COOKER;DIET: EGG-FREE;DIET: VEGETARIAN;PALEO BUDGET-FRIENDLY RECIPES;PALEO KID-FRIENDLY RECIPES
Slow Cooker Curry Chicken https://paleoleap.com/slow-cooker-curry-chicken/ PALEO CHICKEN AND POULTRY RECIPES;PALEO DINNER RECIPES;COOKING: SLOW-COOKER;DIET: EGG-FREE;DIET: NUT-FREE;GOOD FOR LEFTOVERS
Special Sweet Potato Salad https://paleoleap.com/special-sweet-potato-salad/ PALEO SALAD RECIPES;COOKING: FAST COOK;COOKING: FAST PREP;DIET: NUT-FREE;PALEO BUDGET-FRIENDLY RECIPES;GOOD FOR LEFTOVERS;PALEO KID-FRIENDLY RECIPES

当我在 RStudio 中启动我的应用程序时,我的启动屏幕如下所示:

在我点击补充工具栏上的 menuItem -> tbRecipeLs 后,触发 DT::renderDT -> OutDT_RecipeList 函数调用,进而触发 reactive -> Recipe_Inv_Flt() 变量,如 Log_App_Activity UDF:

[1] "DT::renderDT -> OutDT_RecipeList"
[1] "shiny::reactive -> Recipe_Inv_Flt"

这会在仪表板主体中显示 UI 输出 DT::DTOutput -> OutDT_RecipeList

我尝试通过在下拉列表中选择标签过滤器来过滤掉列表中的食谱 selectInput -> InSelect_RecipeTags,见下图:

并根据 Log_App_Activity UDF 控制台输出触发以下 UI 和服务器组件:

[1] "shiny::selectInput -> InSelect_RecipeTags"
[1] "DT::renderDT -> OutDT_RecipeList"

我的期望是,因为 DT::renderDT -> OutDT_RecipeList 被调用,所以 reactive -> Recipe_Inv_Flt() 变量也应该被再次调用,因为它在 DT::renderDT -> OutDT_RecipeList 函数内部,也因为它的依赖性,下拉字段 selectInput -> InSelect_RecipeTags 已更改。然而,情况似乎并非如此,因为数据 table UI 输出 DT::DTOutput -> OutDT_RecipeList 仍然显示所有行 (1,555),但是它确实从Tags 列,基于在下拉字段 selectInput -> InSelect_RecipeTags 中选择的过滤器值,由 ReArrange_Tags DT::renderDT -> OutDT_RecipeList 函数内部的 UDF 调用。我应该怎么做才能使 reactive -> Recipe_Inv_Flt() 变量在每次从 DT::renderDT -> OutDT_RecipeList 函数内部调用时重新计算?

根据@MrFlick 的评论,错字是问题所在: 在reactive -> Recipe_Inv_Flt()定义如下if语句条件:

if(is.null(input$InSelect_Recipe_Tags))

应该是(没有最后一个下划线):

if(is.null(input$InSelect_RecipeTags))

问题已解决,非常感谢!