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

如何实现Shiny应用模块间通信及输入面板同步?

Shiny应用模块同步与数据共享问题

应用结构需求

  1. 一个UI模块(inputPanel)展示侧边栏输入面板,其对应server函数执行后端计算,返回任意数量的数据框列表;
  2. 表格展示模块,基于输入模块的输出渲染表格;
  3. 绘图展示模块,基于输入模块的输出渲染图表。

当前问题

  • 两个标签页(表格/绘图)中的输入面板需要能同步修改同一个reactiveValues对象,并触发对应模块的更新事件;
  • 切换标签页时,两个标签页内的输入面板状态需要保持一致。

原代码模块

ui_module.R

# Contrary to its name, this module is also responsible for executing the 
# backend logic when the submit button is pressed
#-------------------------------------------------------------------------------
library(shiny)


inputPanel <- function(id, i18n) {
  ns <- NS(id)
  
  sidebarPanel(
    # in reality here we have A LOT more elements
    actionButton(
      inputId = ns("submit"),
      label = "Submit"
    )
  )
}


inputServer <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      ns <- session$ns
      
      # Writing important data into session$userData
      session$userData$submit <- reactive(input$submit)
      
      observe({
        # when data in one user interface changes, the other should update so 
        # that they stay consistent! Thus I need to make the two objects 
        # communicate with one another, but I have not been able to make this
        # work.
      })
      
      val <- reactiveValues(data=NULL)
      observe({
        # In reality, this calls a backend function computing a list of data.frames
        val$data <- lapply(1:sample(1:10, 1), function(i) {
          data.frame(X=rnorm(10), Y=rnorm(10))
        })
      }) %>% bindEvent(input$submit)
      
      return(val)
    }
  )
}

table_module.R

library(shiny)


tableTabPanel <- function(id) {
  ns <- NS(id)
  
  tabPanel(
    title="Tables",
    sidebarLayout(
      # From what I understand, this is how I have to utilize modules when I call them from inside other modules so that session$ns gives me the proper id on the server side of things
      inputPanel(paste(id, "navPanel", sep="-")),
      mainPanel(
        uiOutput(ns("tabsetPanel"))
      )
    )
  )
}


tableServer <- function(id, val_outer=NULL) {
  moduleServer(id, function(input, output, session) {
    
    # I tried doing something like this, but clearly it is not working
    # val_inner <- inputServer("navPanel", i18n_r)
    # observe({
    #   val_outer <- val_inner
    # }) %>% bindEvent(val_inner)
    
    # this way, without the inter-communicability it works:
    val <- inputServer("navPanel")
    
    ns <- session$ns
    observe({
      # I am having a hard time creating a MWE. Please understand that I 
      # have tried quite hard to make this minimal example work, but for some 
      # reason, the tables are not rendered. Still, I assume that the 
      # idea and the root of my problem shall be clear to observers since it 
      # is not related to actually rendering any tables
      
      # !is.null to avoid error on startup when val_outer is empty
      if (!is.null(val$data)) {
        lapply(seq_along(val$data), function(i) {
            output[[paste0("table", i)]] <- renderTable(val$data[[i]])
        })
      }
    }) %>% bindEvent(val)
    
    output$tabsetPanel <- renderUI({
      browser()
      tabPanels <-
        if (!is.null(val$data)) {
          lapply(
            X = seq_along(val$data),
            FUN = function(i) {
              tabPanel(title = paste("Tab", i),
                       tableOutput(ns(paste0("table", i))))
            }
          )
        } else {
          list(NULL)
        }
      
      do.call(tabsetPanel, tabPanels)
    })
    
    return(val)
  })
}

plot_module.R

library(shiny)
library(ggplot2)


plotTabPanel <- function(id) {
  ns <- NS(id)
  
  tabPanel(
    title="Plots",
    sidebarLayout(
      inputPanel(paste(id, "navPanel", sep="-"))),
      mainPanel(
        uiOutput(ns("tabsetPanel"))
      )
    )
  )
}


plotServer <- function(id, val_outer) {
  moduleServer(id, function(input, output, session) {
    
    # I tried doing something like this, but clearly it is not working
    val_inner <- inputServer("navPanel", i18n_r)
    observe({
      val_outer <- val_inner
    }) %>% bindEvent(val_inner)
    
    
    ns <- session$ns
    observe({
      # !is.null to avoid error on startup when val_outer is empty
      if (!is.null(val_outer$data)) {
        lapply(seq_along(val_outer$data), function(i) {
          output[[paste0("table", i)]] <-
            renderPlot(ggplot(data=val$data[[i]]) + geom_point(x=X, y=Y))
        })
      }
    })

    output$tabsetPanel <- renderUI({
      tabList <-   
        if (!is.null(val_outer$data)) {
          lapply(seq_along(val_outer$data), function(i) { 
            tabPanel(title = paste("Tab", i),
                     tableOutput(ns(paste0("table", i))))
          })
        } else {
          tabPanel(title = "Sample title")
        }
      
      do.call(tabsetPanel, tabList)
    })

    return(val_outer)
  })
}

main.R

library(shiny)


# some reactiveValues containing various fields
val <- reactiveValues(data=NULL)  # and some more values


ui <- navbarPage(
  title = "title",
  
  tableTabPanel("tableTab"),
  # plotTabPanel("plotTab")
)


server <- function(input, output, session) {
  # The idea is to allow the user to access the input panel from both tabs. For this I need to observe, throughout the "lifecycle" of my app, whether changes to val have occured
  val <- tableServer("tableTab", val)
  # val <- plotServer("plotTab", val)
}


shinyApp(ui=ui, server=server)

解决方案

核心思路是将输入模块的状态和计算逻辑抽离为全局共享实例,避免每个标签页独立初始化输入模块,让所有输入面板绑定到同一数据源和逻辑。

1. 重构输入模块:分离UI与共享逻辑

修改ui_module.R,让UI只负责渲染,计算逻辑移到共享server模块:

library(shiny)

inputPanel <- function(id) {
  ns <- NS(id)
  
  sidebarPanel(
    actionButton(
      inputId = ns("submit"),
      label = "提交"
    )
    # 保留原有其他输入控件
  )
}

# 共享输入逻辑:处理提交事件与数据计算
sharedInputServer <- function(id) {
  moduleServer(
    id,
    function(input, output, session) {
      val <- reactiveValues(data = NULL)
      
      # 提交事件触发计算
      observeEvent(input$submit, {
        val$data <- lapply(1:sample(1:10, 1), function(i) {
          data.frame(X = rnorm(10), Y = rnorm(10))
        })
      })
      
      return(val)
    }
  )
}

2. 修改表格模块:绑定共享数据源

修改table_module.R,不再内部初始化输入模块,接收全局共享的val:

library(shiny)

tableTabPanel <- function(id) {
  ns <- NS(id)
  
  tabPanel(
    title = "表格",
    sidebarLayout(
      inputPanel(paste(id, "input", sep = "-")),
      mainPanel(
        uiOutput(ns("tabsetPanel"))
      )
    )
  )
}

tableServer <- function(id, shared_val) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    
    # 监听共享数据变化,渲染表格
    observeEvent(shared_val$data, {
      if (!is.null(shared_val$data)) {
        lapply(seq_along(shared_val$data), function(i) {
          output[[paste0("table", i)]] <- renderTable(shared_val$data[[i]])
        })
      }
    })
    
    output$tabsetPanel <- renderUI({
      tabPanels <- if (!is.null(shared_val$data)) {
        lapply(seq_along(shared_val$data), function(i) {
          tabPanel(
            title = paste("表格", i),
            tableOutput(ns(paste0("table", i)))
          )
        })
      } else {
        list(tabPanel(title = "暂无数据"))
      }
      do.call(tabsetPanel, tabPanels)
    })
    
    # 当前输入面板的提交按钮绑定到共享数据更新
    observeEvent(input$submit, {
      shared_val$data <- lapply(1:sample(1:10, 1), function(i) {
        data.frame(X = rnorm(10), Y = rnorm(10))
      })
    })
  })
}

3. 修改绘图模块:绑定共享数据源

修改plot_module.R,同样接收全局共享的val:

library(shiny)
library(ggplot2)

plotTabPanel <- function(id) {
  ns <- NS(id)
  
  tabPanel(
    title = "绘图",
    sidebarLayout(
      inputPanel(paste(id, "input", sep = "-")),
      mainPanel(
        uiOutput(ns("tabsetPanel"))
      )
    )
  )
}

plotServer <- function(id, shared_val) {
  moduleServer(id, function(input, output, session) {
    ns <- session$ns
    
    # 监听共享数据变化,渲染图表
    observeEvent(shared_val$data, {
      if (!is.null(shared_val$data)) {
        lapply(seq_along(shared_val$data), function(i) {
          output[[paste0("plot", i)]] <- renderPlot({
            ggplot(data = shared_val$data[[i]]) + 
              geom_point(aes(x = X, y = Y))
          })
        })
      }
    })
    
    output$tabsetPanel <- renderUI({
      tabPanels <- if (!is.null(shared_val$data)) {
        lapply(seq_along(shared_val$data), function(i) {
          tabPanel(
            title = paste("图表", i),
            plotOutput(ns(paste0("plot", i)))
          )
        })
      } else {
        list(tabPanel(title = "暂无数据"))
      }
      do.call(tabsetPanel, tabPanels)
    })
    
    # 当前输入面板的提交按钮绑定到共享数据更新
    observeEvent(input$submit, {
      shared_val$data <- lapply(1:sample(1:10, 1), function(i) {
        data.frame(X = rnorm(10), Y = rnorm(10))
      })
    })
  })
}

4. 主程序:初始化共享逻辑并传递给子模块

修改main.R,先初始化共享输入逻辑,再将共享val传递给两个子模块:

library(shiny)

ui <- navbarPage(
  title = "数据展示平台",
  tableTabPanel("tableTab"),
  plotTabPanel("plotTab")
)

server <- function(input, output, session) {
  # 初始化全局共享的输入逻辑与数据
  shared_val <- sharedInputServer("sharedInput")
  
  # 将共享数据传递给表格、绘图模块
  tableServer("tableTab", shared_val)
  plotServer("plotTab", shared_val)
}

shinyApp(ui = ui, server = server)

关键说明

  • 共享状态:通过全局shared_val对象统一管理数据源,任何输入面板的提交操作都会更新该对象,触发所有依赖模块同步更新;
  • 输入同步:若输入面板包含多控件,可在共享模块中存储所有输入值,通过updateXXXInput函数同步所有标签页的控件状态;
  • 事件统一:所有输入面板的提交按钮绑定到同一数据更新逻辑,确保操作一致性。

内容的提问来源于stack exchange,提问作者J. Grünenwald

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 18:59:55