0Pricing
R Academy · レッスン

コード再利用のための Shiny モジュール

UI とサーバーロジックを名前空間付きの再利用可能なモジュールにカプセル化します。

「コード再利用のための Shiny モジュール」はCoddyKit上の無料R Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはR Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 R Academyコースには全4レッスンが含まれています。

なぜ Shiny モジュールを使うのか

Shinyアプリが大きくなると、UIとサーバーのコードを1つのファイルにまとめておくのは管理が難しくなります。モジュールは、名前空間付きのIDを持つ、自己完結型のShiny UIとサーバーロジックです。同じモジュールを1つのアプリ内でIDの衝突なく何度も再利用でき、モジュールを個別にテストすることもできます。

# Problem: without modules, ID conflicts arise
# ui <- fluidPage(
#   selectInput('dataset', ...),  # used by plot1 AND plot2!
#   selectInput('dataset', ...)   # duplicate ID — BROKEN
# )

# With modules: each instance has its own namespaced IDs
# plotModule('plot1', ...)  ->  input$plot1-dataset
# plotModule('plot2', ...)  ->  input$plot2-dataset

NS() — 名前空間関数

すべてのモジュールUI関数は、ns <- NS(id)から始めます。Shinyに渡すすべてのUI要素のIDは、ns()で囲む必要があります。これにより各IDの先頭にid-が付加され、モジュールのインスタンス間の衝突を防ぐ名前空間が作成されます。

# Module UI function
filter_plot_ui <- function(id) {
  ns <- NS(id)  # create the namespace function

  tagList(
    selectInput(ns('dataset'), 'Choose Dataset:',
                choices = c('mtcars', 'iris', 'airquality')),
    sliderInput(ns('n_rows'), 'Rows to show:', 1, 50, 20),
    plotOutput(ns('scatter_plot'))
  )
}

moduleServer() — サーバー関数

moduleServer(id, function(input, output, session) {...})は、モジュールのサーバーロジックを定義する現代的な方法(Shiny 1.5以降)です。関数内ではinput、output、sessionが自動的に名前空間化されるため、input$plot1-datasetではなくinput$datasetにアクセスします。

# Module server function
filter_plot_server <- function(id) {
  moduleServer(id, function(input, output, session) {

    data <- reactive({
      # input$dataset is already namespaced to this instance
      head(get(input$dataset), input$n_rows)
    })

    output$scatter_plot <- renderPlot({
      df <- data()
      plot(df[[1]], df[[2]],
           xlab = names(df)[1], ylab = names(df)[2])
    })
  })
}

アプリでモジュールを使用する

uiではモジュールUI関数を、serverではモジュールサーバー関数を、どちらも同じid文字列を使って呼び出します。同じモジュールを異なるIDで何度も呼び出し、独立したインスタンスを作成できます。

# Main app using the module twice
ui <- fluidPage(
  h2('Plot 1'),
  filter_plot_ui('plot1'),   # instance 1
  hr(),
  h2('Plot 2'),
  filter_plot_ui('plot2')    # instance 2 — no ID conflicts!
)

server <- function(input, output, session) {
  filter_plot_server('plot1')  # wire up instance 1
  filter_plot_server('plot2')  # wire up instance 2
}

shinyApp(ui, server)

モジュールUIにパラメーターを渡す

モジュールUI関数は通常のR関数にすぎません。id以外の追加パラメーターを指定すると、作成時に各インスタンスの外観や動作をカスタマイズできます。これらはUIの構築時に一度だけ評価されます。

# Module UI with extra parameters
summary_table_ui <- function(id, title = 'Summary', height = '300px') {
  ns <- NS(id)
  tagList(
    h4(title),
    div(
      style = paste0('height:', height, '; overflow-y: auto;'),
      DTOutput(ns('tbl'))
    )
  )
}

# Use with custom titles
summary_table_ui('train_tbl', title = 'Training Data', height = '400px')
summary_table_ui('test_tbl',  title = 'Test Data',     height = '200px')

モジュールにリアクティブを渡す

モジュールサーバー関数は、リアクティブな値や式をパラメーターとして受け取れます。これにより、親アプリから子モジュールへデータを渡せます。モジュール内では、リアクティブを関数として呼び出すと現在の値を取得できます。

# Module that accepts a reactive as input
chart_module_server <- function(id, data_reactive) {
  moduleServer(id, function(input, output, session) {
    output$chart <- renderPlot({
      df <- data_reactive()   # call the reactive passed in
      ggplot2::ggplot(df, ggplot2::aes(x = x, y = y)) +
        ggplot2::geom_point(colour = input$colour)
    })
  })
}

# In main server:
server <- function(input, output, session) {
  shared_data <- reactive({ load_data(input$source) })
  chart_module_server('chart1', data_reactive = shared_data)
  chart_module_server('chart2', data_reactive = shared_data)
}

モジュールからリアクティブを返す

モジュールサーバー関数は、親へリアクティブな値を返せます。これにより、子モジュールから親へ情報を伝えられます。moduleServer()からリアクティブな値またはリアクティブな値のリストを返し、親のサーバー関数で受け取ってください。

# Module that returns a reactive to the parent
filter_module_server <- function(id, all_data) {
  moduleServer(id, function(input, output, session) {

    # Return the filtered data reactive
    filtered <- reactive({
      all_data[all_data$group == input$group_filter, ]
    })

    return(filtered)  # parent can use this reactive
  })
}

# In main server:
server <- function(input, output, session) {
  raw_data <- reactive({ read.csv('data.csv') })

  # filtered_data is a reactive returned from the module
  filtered_data <- filter_module_server('filter1', raw_data)

  output$main_plot <- renderPlot({ plot(filtered_data()) })
}

モジュールのファイル構成

大規模なアプリでは、各モジュールをR/フォルダー内の独立したファイルに配置します。Shinyはアプリの読み込み時にR/内のすべてのファイルを自動的に読み込みます。これにより各モジュールが自己完結し、shinytest2パッケージを使って個別にテストしやすくなります。

# Recommended project structure:
# myapp/
#   app.R                   # main app: source modules + wire up
#   R/
#     mod_filter_plot.R     # filter_plot_ui() + filter_plot_server()
#     mod_summary_table.R   # summary_table_ui() + summary_table_server()
#     mod_download.R        # download_ui() + download_server()
#   tests/
#     testthat/test-mod_filter_plot.R

# In app.R:
library(shiny)
# source('R/mod_filter_plot.R')  # not needed if in R/ folder
ui     <- fluidPage(filter_plot_ui('p1'))
server <- function(input, output, session) { filter_plot_server('p1') }
shinyApp(ui, server)

入れ子になったモジュール

モジュールには他のモジュールを含めることができます。親モジュールは、session引数を使って、自身のsessionの名前空間を子モジュールの呼び出しに渡します。入れ子の各レベルで名前空間のプレフィックスが追加され、outer-inner-element_idのようになります。

# Outer module uses an inner module
outer_server <- function(id) {
  moduleServer(id, function(input, output, session) {

    # Call an inner module using this module's session
    inner_result <- inner_module_server(
      id      = 'inner',
      session = session  # passes the namespaced session
    )

    output$combined <- renderText({
      paste('Inner result:', inner_result())
    })
  })
}

# ID chain: outer -> inner
# Full ID: outer-inner-element

shinytest2でモジュールをテストする

shinytest2パッケージを使うと、モジュールを最小限のアプリでラップして自動テストを作成できます。AppDriverでブラウザーを操作し、入力を設定して出力値を検証できます。実際のブラウザーセッションは必要ありません。

library(shinytest2)

# Wrap the module in a testable app
test_that('filter_plot module filters correctly', {
  test_app <- shinyApp(
    ui     = fluidPage(filter_plot_ui('test')),
    server = function(input, output, session) {
      filter_plot_server('test')
    }
  )

  app <- AppDriver$new(test_app)
  app$set_inputs('test-dataset' = 'iris')  # namespaced input
  app$wait_for_idle()

  # Assert plot was rendered
  expect_true(!is.null(app$get_value(output = 'test-scatter_plot')))
})

モジュール間の通信パターン

モジュールと親アプリの間で使われる通信パターンのまとめ:

  • 親からモジュール: リアクティブをパラメーターとしてモジュールサーバーに渡します。
  • モジュールから親: moduleServer()からリアクティブを返します。
  • 兄弟モジュール間: 親が共有状態(reactiveValues)を保持し、それを各モジュールに渡します。
  • グローバル状態: 親で定義したreactiveValuesを使用し、その参照を下位へ渡します。
# Sibling module communication via parent state
server <- function(input, output, session) {
  shared <- reactiveValues(selected_row = NULL)

  # Table module sets the selection
  table_module_server('tbl', shared_state = shared)

  # Detail module reads the selection
  detail_module_server('detail', shared_state = shared)

  # Both modules communicate through 'shared' reactiveValues
  # Table sets shared$selected_row; Detail reads it
}

確認問題

モジュールUI関数内のすべてのUI要素のIDをns()で囲む必要があるのはなぜですか。

Shinyモジュールのまとめ

コード再利用のためのShinyモジュールの要点:

  • モジュールは、すべてのIDをNS(id)で名前空間化することで、IDの衝突を防ぎます。
  • モジュールUI: ns <- NS(id)を持つ通常の関数で、すべてのIDをns()で囲みます。
  • モジュールサーバー: moduleServer(id, function(input, output, session) {...})です。
  • リアクティブを関数パラメーターとしてモジュールに渡し、リアクティブを返して上位へ情報を伝えます。
  • 同じモジュールを異なるIDで何度も呼び出し、独立したインスタンスを作成できます。
  • モジュールをR/mod_*.Rファイルに整理すると、ShinyがR/フォルダーを自動的に読み込みます。
  • shinytest2::AppDriverでモジュールをテストします。
# Complete module example
my_module_ui <- function(id) {
  ns <- NS(id)
  tagList(selectInput(ns('var'), 'Variable:', choices = names(mtcars)),
          plotOutput(ns('hist')))
}

my_module_server <- function(id, data) {
  moduleServer(id, function(input, output, session) {
    output$hist <- renderPlot(hist(data()[[input$var]]))
  })
}

# Use it:
ui <- fluidPage(my_module_ui('m1'), my_module_ui('m2'))
server <- function(input, output, session) {
  d <- reactive(mtcars)
  my_module_server('m1', d)
  my_module_server('m2', d)
}
shinyApp(ui, server)

よくある質問

「コード再利用のための Shiny モジュール」レッスンは無料ですか?

はい。「コード再利用のための Shiny モジュール」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、R Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 R Academyコースには全4レッスンが含まれています。

「コード再利用のための Shiny モジュール」で何を学びますか?

UI とサーバーロジックを名前空間付きの再利用可能なモジュールにカプセル化します。 ブラウザで直接実行するハンズオンコードでR Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

R Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのR Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「コード再利用のための Shiny モジュール」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このR Academyレッスンでコードを書いて実行できますか?

はい。すべてのR Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. リアクティブプログラミングの深掘り
  2. コード再利用のための Shiny モジュール
  3. renderUI と insertUI による動的 UI
  4. Shiny アプリをデプロイする
← R Academyに戻る