Creating Shiny Packages: Complete Guide to Professional Package Development

Master Advanced Package Architecture, Distribution, and Maintenance for Scalable Shiny Applications

Learn to create professional Shiny packages with proper architecture, documentation, testing, and distribution. Master package development workflows, dependency management, and best practices for building reusable, maintainable Shiny components and applications.

Tools
Author
Affiliation
Published

May 23, 2025

Modified

June 19, 2025

Keywords

shiny package development, R package creation, shiny package tutorial, R package development, shiny modules package, distribute shiny apps

Key Takeaways

Tip
  • Professional Package Architecture: Well-structured Shiny packages enable code reusability, easier maintenance, and collaborative development across teams and organizations
  • Modular Component Design: Package-based development promotes modular architecture with clear interfaces, making complex applications more manageable and testable
  • Distribution and Versioning: Professional package workflows enable controlled distribution, semantic versioning, and dependency management for reliable deployment across environments
  • Documentation Excellence: Comprehensive documentation including vignettes, function references, and examples ensures packages are accessible and maintainable by development teams
  • Quality Assurance Integration: Package development workflows integrate testing, continuous integration, and quality checks that ensure reliability and professional standards

Introduction

Creating Shiny packages represents the pinnacle of professional Shiny development, transforming individual applications into reusable, distributable components that can be shared across teams, organizations, and the broader R community. Package development enables you to build libraries of Shiny modules, utility functions, and complete applications that follow software engineering best practices for maintainability, testing, and documentation.



This comprehensive guide covers the complete spectrum of Shiny package development, from initial project setup and architecture design to advanced distribution strategies and long-term maintenance workflows. You’ll master the techniques that professional R developers use to create robust, well-documented packages that integrate seamlessly into larger development ecosystems and provide reliable foundations for complex applications.

The package development skills you’ll learn are essential for any developer building Shiny applications at scale, whether you’re creating internal company libraries, contributing to open-source projects, or building commercial Shiny solutions. These professional development practices ensure your code is reusable, maintainable, and meets the quality standards expected in production software development environments.

Understanding Shiny Package Architecture

Shiny packages require careful architectural planning to balance functionality, maintainability, and ease of use. The structure differs from standard R packages due to the unique requirements of reactive programming and web application components.

flowchart TD
    A[Shiny Package Architecture] --> B[Package Structure]
    A --> C[Module System]
    A --> D[Asset Management]
    A --> E[Documentation]
    
    B --> F[R/ Functions]
    B --> G[inst/ Resources]
    B --> H[man/ Documentation]
    B --> I[tests/ Testing]
    
    C --> J[UI Modules]
    C --> K[Server Modules]
    C --> L[Utility Functions]
    
    D --> M[CSS Files]
    D --> N[JavaScript]
    D --> O[Images/Icons]
    
    E --> P[Function Help]
    E --> Q[Vignettes]
    E --> R[README]
    E --> S[News/Changelog]
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style E fill:#fce4ec

Core Package Components

Modular Design Philosophy

Shiny packages should be built around modular components that can be combined and reused in different contexts while maintaining clear separation of concerns. This approach enables developers to use only the components they need while ensuring consistent behavior across applications.

Asset Integration Strategy

Proper management of CSS, JavaScript, and other web assets ensures consistent styling and functionality across different deployment environments. Assets must be packaged correctly to work in both development and installed package contexts.

Dependency Management Framework

Careful specification of package dependencies and version requirements ensures reliable installation and operation across different R environments, while minimizing conflicts with other packages.

Documentation Standards

Comprehensive documentation including function references, vignettes, and examples makes packages accessible to other developers and ensures long-term maintainability as teams and requirements evolve.

Setting Up Your Package Development Environment

Initial Project Setup

Creating a professional Shiny package requires proper project initialization with all necessary directories and configuration files:

# Install required development packages
install.packages(c("devtools", "usethis", "roxygen2", "testthat", "pkgdown"))

# Create new package
usethis::create_package("~/myShinyPackage")

# Set up package structure for Shiny development
usethis::use_mit_license()
usethis::use_roxygen_md()
usethis::use_testthat()
usethis::use_package_doc()
usethis::use_readme_md()

Essential Package Structure

A well-organized Shiny package follows this directory structure:

myShinyPackage/
├── DESCRIPTION          # Package metadata and dependencies
├── NAMESPACE           # Package exports (auto-generated)
├── LICENSE             # License file
├── README.md           # Package overview and usage
├── NEWS.md             # Version history and changes
├── R/                  # R source code
│   ├── module-ui.R     # UI module functions
│   ├── module-server.R # Server module functions
│   ├── utils.R         # Utility functions
│   └── package.R       # Package documentation
├── man/                # Help documentation (auto-generated)
├── tests/              # Unit tests
│   └── testthat/
├── inst/               # Installed files
│   ├── www/            # Web assets (CSS, JS, images)
│   └── examples/       # Example applications
├── vignettes/          # Long-form documentation
└── data-raw/           # Raw data and processing scripts

Package Configuration

Create a properly configured DESCRIPTION file:

# Example DESCRIPTION file content
Package: myShinyPackage
Type: Package
Title: Professional Shiny Components and Utilities
Version: 0.1.0
Author: Your Name <your.email@example.com>
Maintainer: Your Name <your.email@example.com>
Description: A collection of reusable Shiny modules and utilities
    for building professional interactive web applications.
    Includes data visualization components, input controls,
    and theming utilities.
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.2.3
VignetteBuilder: knitr
Depends:
    R (>= 4.1.0)
Imports:
    shiny (>= 1.7.0),
    htmltools (>= 0.5.0),
    bslib (>= 0.4.0),
    DT (>= 0.20)
Suggests:
    testthat (>= 3.0.0),
    knitr (>= 1.33),
    rmarkdown (>= 2.11)
URL: https://github.com/yourusername/myShinyPackage
BugReports: https://github.com/yourusername/myShinyPackage/issues

Building Modular Components

Creating Reusable Shiny Modules

Shiny modules are the foundation of package-based development. Here’s how to create well-structured, reusable modules:

# R/data-table-module.R

#' Data Table Module UI
#'
#' @description UI function for interactive data table display
#'
#' @param id Character string, module namespace ID
#' @param height Character string, table height (default: "400px")
#' @param show_filters Logical, whether to show filter controls
#'
#' @return Shiny UI elements
#' @export
#'
#' @examples
#' if (interactive()) {
#'   library(shiny)
#'   
#'   ui <- fluidPage(
#'     data_table_ui("example")
#'   )
#'   
#'   server <- function(input, output, session) {
#'     data_table_server("example", reactive(mtcars))
#'   }
#'   
#'   shinyApp(ui, server)
#' }
data_table_ui <- function(id, height = "400px", show_filters = TRUE) {
  ns <- shiny::NS(id)
  
  shiny::tagList(
    # Include package CSS
    include_package_css(),
    
    shiny::div(
      class = "data-table-module",
      
      # Filter controls
      if (show_filters) {
        shiny::div(
          class = "filter-controls",
          shiny::fluidRow(
            shiny::column(6,
              shiny::selectInput(
                ns("filter_column"), 
                "Filter Column:", 
                choices = NULL,
                width = "100%"
              )
            ),
            shiny::column(6,
              shiny::textInput(
                ns("filter_value"), 
                "Filter Value:",
                width = "100%"
              )
            )
          ),
          shiny::div(
            class = "filter-actions",
            shiny::actionButton(ns("apply_filter"), "Apply Filter", class = "btn-primary"),
            shiny::actionButton(ns("reset_filter"), "Reset", class = "btn-secondary")
          )
        )
      },
      
      # Data table
      shiny::div(
        class = "table-container",
        DT::dataTableOutput(ns("table"), height = height)
      )
    )
  )
}

#' Data Table Module Server
#'
#' @description Server function for interactive data table display
#'
#' @param id Character string, module namespace ID
#' @param data Reactive data frame to display
#' @param options List of DT options (optional)
#'
#' @return List of reactive values including filtered data and selections
#' @export
data_table_server <- function(id, data, options = list()) {
  shiny::moduleServer(id, function(input, output, session) {
    
    # Reactive values for module state
    values <- shiny::reactiveValues(
      filtered_data = NULL,
      selected_rows = NULL
    )
    
    # Update column choices when data changes
    shiny::observe({
      shiny::req(data())
      
      column_choices <- names(data())
      shiny::updateSelectInput(
        session, 
        "filter_column", 
        choices = column_choices,
        selected = column_choices[1]
      )
    })
    
    # Apply filter logic
    filtered_data <- shiny::reactive({
      df <- data()
      shiny::req(df)
      
      if (!is.null(input$filter_column) && 
          !is.null(input$filter_value) && 
          input$filter_value != "") {
        
        filter_col <- input$filter_column
        filter_val <- input$filter_value
        
        if (filter_col %in% names(df)) {
          if (is.character(df[[filter_col]])) {
            df <- df[grepl(filter_val, df[[filter_col]], ignore.case = TRUE), ]
          } else if (is.numeric(df[[filter_col]])) {
            numeric_val <- suppressWarnings(as.numeric(filter_val))
            if (!is.na(numeric_val)) {
              df <- df[df[[filter_col]] == numeric_val, ]
            }
          }
        }
      }
      
      values$filtered_data <- df
      return(df)
    })
    
    # Reset filter handler
    shiny::observeEvent(input$reset_filter, {
      shiny::updateTextInput(session, "filter_value", value = "")
    })
    
    # Render data table
    output$table <- DT::renderDataTable({
      filtered_data()
    }, options = c(
      list(
        pageLength = 25,
        scrollX = TRUE,
        dom = 'Bfrtip',
        buttons = c('copy', 'csv', 'excel'),
        responsive = TRUE
      ),
      options
    ))
    
    # Track selected rows
    shiny::observe({
      values$selected_rows <- input$table_rows_selected
    })
    
    # Return reactive values for external use
    return(list(
      filtered_data = filtered_data,
      selected_rows = shiny::reactive(values$selected_rows)
    ))
  })
}

Asset Management System

Proper asset management ensures your package’s CSS, JavaScript, and other resources work correctly across different environments:

# R/assets.R

#' Include Package CSS
#'
#' @description Include package-specific CSS styles
#'
#' @return HTML head content with CSS link
#' @export
include_package_css <- function() {
  # Add resource path for package assets
  pkg_name <- utils::packageName()
  resource_path <- system.file("www", package = pkg_name)
  
  if (dir.exists(resource_path)) {
    shiny::addResourcePath(pkg_name, resource_path)
    
    htmltools::tags$head(
      htmltools::tags$link(
        rel = "stylesheet",
        type = "text/css",
        href = paste0(pkg_name, "/css/package-styles.css")
      )
    )
  } else {
    # Fallback inline styles if package assets not found
    htmltools::tags$style(
      type = "text/css",
      "
      .data-table-module {
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        margin: 15px 0;
      }
      .filter-controls {
        background-color: #f8f9fa;
        padding: 15px;
        border-radius: 5px;
        margin-bottom: 15px;
      }
      .filter-actions {
        margin-top: 10px;
        text-align: right;
      }
      .filter-actions .btn {
        margin-left: 5px;
      }
      .table-container {
        border: 1px solid #dee2e6;
        border-radius: 5px;
        overflow: hidden;
      }
      "
    )
  }
}

#' Create HTML Dependencies
#'
#' @description Create HTML dependencies for package assets
#'
#' @return HTML dependency object
#' @export
create_package_dependencies <- function() {
  pkg_name <- utils::packageName()
  pkg_version <- utils::packageVersion(pkg_name)
  
  htmltools::htmlDependency(
    name = paste0(pkg_name, "-assets"),
    version = as.character(pkg_version),
    src = c(file = system.file("www", package = pkg_name)),
    stylesheet = "css/package-styles.css",
    script = "js/package-utils.js"
  )
}

Create the actual CSS file in inst/www/css/package-styles.css:

/* Package-specific CSS styles */
.data-table-module {
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  margin: 15px 0;
}

.filter-controls {
  background-color: #f8f9fa;
  padding: 15px;
  border-radius: 5px;
  margin-bottom: 15px;
  border: 1px solid #e9ecef;
}

.filter-actions {
  margin-top: 10px;
  text-align: right;
}

.filter-actions .btn {
  margin-left: 5px;
  padding: 6px 12px;
  border-radius: 4px;
  border: 1px solid transparent;
  cursor: pointer;
  transition: all 0.2s ease;
}

.btn-primary {
  background-color: #007bff;
  border-color: #007bff;
  color: white;
}

.btn-primary:hover {
  background-color: #0056b3;
  border-color: #0056b3;
}

.btn-secondary {
  background-color: #6c757d;
  border-color: #6c757d;
  color: white;
}

.btn-secondary:hover {
  background-color: #545b62;
  border-color: #545b62;
}

.table-container {
  border: 1px solid #dee2e6;
  border-radius: 5px;
  overflow: hidden;
}

/* Responsive adjustments */
@media (max-width: 768px) {
  .filter-controls {
    padding: 10px;
  }
  
  .filter-actions {
    text-align: center;
  }
  
  .filter-actions .btn {
    display: block;
    width: 100%;
    margin: 5px 0;
  }
}

Documentation and Testing

Comprehensive Documentation Strategy

Professional packages require multiple levels of documentation:

# R/package.R

#' myShinyPackage: Professional Shiny Components
#'
#' @description
#' A collection of reusable Shiny modules and utilities for building
#' professional interactive web applications. The package provides
#' data visualization components, input controls, and theming utilities
#' that follow modern web development best practices.
#'
#' @section Main Functions:
#' \describe{
#'   \item{data_table_ui, data_table_server}{Interactive data table module}
#'   \item{include_package_css}{Asset management utilities}
#'   \item{create_package_dependencies}{HTML dependency management}
#' }
#'
#' @section Getting Started:
#' To get started with myShinyPackage, see the package vignette:
#' \code{vignette("introduction", package = "myShinyPackage")}
#'
#' @docType package
#' @name myShinyPackage
#' @import shiny
#' @import htmltools
#' @importFrom DT dataTableOutput renderDataTable
NULL

Testing Framework

Create comprehensive tests for your package components:

# tests/testthat/test-data-table-module.R

test_that("data_table_ui creates proper UI structure", {
  ui <- data_table_ui("test")
  
  # Test that UI is a valid shiny.tag.list
  expect_s3_class(ui, "shiny.tag.list")
  
  # Convert to character for content testing
  ui_html <- as.character(ui)
  
  # Test for key components
  expect_true(grepl("data-table-module", ui_html))
  expect_true(grepl("test-filter_column", ui_html))
  expect_true(grepl("test-table", ui_html))
})

test_that("data_table_ui respects show_filters parameter", {
  # With filters
  ui_with_filters <- data_table_ui("test", show_filters = TRUE)
  ui_html_with <- as.character(ui_with_filters)
  expect_true(grepl("filter-controls", ui_html_with))
  
  # Without filters
  ui_without_filters <- data_table_ui("test", show_filters = FALSE)
  ui_html_without <- as.character(ui_without_filters)
  expect_false(grepl("filter-controls", ui_html_without))
})

test_that("data_table_server handles data correctly", {
  # Create test data
  test_data <- reactive({
    data.frame(
      name = c("Alice", "Bob", "Charlie"),
      age = c(25, 30, 35),
      city = c("New York", "Boston", "Chicago"),
      stringsAsFactors = FALSE
    )
  })
  
  # Test server function
  testServer(data_table_server, args = list(data = test_data), {
    
    # Test initial state
    session$flushReact()
    expect_equal(nrow(values$filtered_data), 3)
    
    # Test column choices update
    expect_equal(
      session$getReturned()$filtered_data(),
      test_data()
    )
    
    # Test filtering
    session$setInputs(
      filter_column = "city",
      filter_value = "New"
    )
    session$flushReact()
    
    filtered_result <- session$getReturned()$filtered_data()
    expect_equal(nrow(filtered_result), 1)
    expect_equal(filtered_result$name[1], "Alice")
  })
})

test_that("asset functions work correctly", {
  # Test CSS inclusion
  css_output <- include_package_css()
  expect_s3_class(css_output, "shiny.tag.list")
  
  # Test dependency creation
  deps <- create_package_dependencies()
  expect_s3_class(deps, "html_dependency")
  expect_equal(deps$name, "myShinyPackage-assets")
})

Integration Testing

Create tests that verify complete workflows:

# tests/testthat/test-integration.R

test_that("complete module workflow works", {
  # Create test application
  test_app <- function() {
    ui <- shiny::fluidPage(
      data_table_ui("test")
    )
    
    server <- function(input, output, session) {
      test_data <- reactive({
        data.frame(
          id = 1:5,
          value = rnorm(5),
          category = sample(c("A", "B"), 5, replace = TRUE)
        )
      })
      
      data_table_server("test", test_data)
    }
    
    shiny::shinyApp(ui, server)
  }
  
  # Test with shinytest2 (if available)
  skip_if_not_installed("shinytest2")
  
  app <- shinytest2::AppDriver$new(test_app())
  
  # Test initial load
  app$wait_for_idle()
  expect_true(app$get_value(output = "test-table") != "")
  
  # Test filtering
  app$set_inputs(`test-filter_column` = "category")
  app$set_inputs(`test-filter_value` = "A")
  app$click("test-apply_filter")
  app$wait_for_idle()
  
  # Verify filter was applied
  table_data <- app$get_value(output = "test-table")
  expect_true(!is.null(table_data))
  
  app$stop()
})

Package Distribution and Maintenance

Version Management

Implement semantic versioning and release management:

# R/version-management.R (internal functions)

#' Update Package Version
#'
#' @param type Version increment type: "major", "minor", or "patch"
#' @param dev Logical, whether to add development suffix
#'
#' @return New version string
#' @keywords internal
update_package_version <- function(type = "patch", dev = FALSE) {
  
  # Read DESCRIPTION file
  desc_path <- "DESCRIPTION"
  if (!file.exists(desc_path)) {
    stop("DESCRIPTION file not found")
  }
  
  desc_content <- readLines(desc_path)
  version_line <- grep("^Version:", desc_content)
  
  if (length(version_line) == 0) {
    stop("Version field not found in DESCRIPTION")
  }
  
  current_version <- gsub("Version:\\s*", "", desc_content[version_line])
  version_parts <- as.numeric(strsplit(current_version, "\\.")[[1]][1:3])
  
  # Increment version
  switch(type,
    "major" = {
      version_parts[1] <- version_parts[1] + 1
      version_parts[2:3] <- 0
    },
    "minor" = {
      version_parts[2] <- version_parts[2] + 1
      version_parts[3] <- 0
    },
    "patch" = {
      version_parts[3] <- version_parts[3] + 1
    }
  )
  
  new_version <- paste(version_parts, collapse = ".")
  if (dev) new_version <- paste0(new_version, ".9000")
  
  # Update DESCRIPTION
  desc_content[version_line] <- paste("Version:", new_version)
  writeLines(desc_content, desc_path)
  
  message("Version updated to: ", new_version)
  return(new_version)
}

CRAN Preparation

Prepare your package for CRAN submission:

# Development workflow for CRAN preparation

# 1. Check package structure and documentation
devtools::check()

# 2. Run additional checks
devtools::check_win_devel()  # Windows check
rhub::check_for_cran()       # Multiple platform check

# 3. Update documentation
devtools::document()
pkgdown::build_site()

# 4. Final preparations
devtools::spell_check()      # Check spelling
urlchecker::url_check()      # Check URLs

Create a CRAN submission checklist:

# CRAN Submission Checklist

## Pre-submission
- [ ] All examples run without errors
- [ ] All tests pass
- [ ] R CMD check passes with no errors, warnings, or notes
- [ ] Package works on multiple R versions
- [ ] Documentation is complete and accurate
- [ ] NEWS.md is updated
- [ ] Version number is incremented appropriately

## Submission
- [ ] Submit via `devtools::submit_cran()`
- [ ] Respond to reviewer feedback promptly
- [ ] Create GitHub release after acceptance

## Post-submission
- [ ] Update development version
- [ ] Plan next release cycle

Common Issues and Solutions

Issue 1: Module Namespace Conflicts

Problem: Module IDs conflict when multiple instances are used in the same application.

Solution:

# Ensure unique namespaces for module instances
create_unique_module_id <- function(base_id, instance = NULL) {
  
  if (is.null(instance)) {
    # Generate unique instance identifier
    instance <- format(Sys.time(), "%Y%m%d_%H%M%S")
  }
  
  unique_id <- paste(base_id, instance, sep = "_")
  return(unique_id)
}

# Usage in application
ui <- fluidPage(
  data_table_ui(create_unique_module_id("table", "main")),
  data_table_ui(create_unique_module_id("table", "secondary"))
)

Issue 2: Asset Loading Problems

Problem: CSS and JavaScript assets don’t load correctly in different environments.

Solution:

# Robust asset loading with fallbacks
safe_include_assets <- function() {
  
  pkg_name <- utils::packageName()
  
  # Try to load package assets
  tryCatch({
    resource_path <- system.file("www", package = pkg_name)
    
    if (dir.exists(resource_path)) {
      shiny::addResourcePath(pkg_name, resource_path)
      
      return(htmltools::tags$head(
        htmltools::tags$link(
          rel = "stylesheet",
          href = paste0(pkg_name, "/css/package-styles.css")
        )
      ))
    }
  }, error = function(e) {
    warning("Could not load package assets: ", e$message)
  })
  
  # Fallback to inline styles
  return(htmltools::tags$style(
    type = "text/css",
    get_fallback_css()
  ))
}

get_fallback_css <- function() {
  "
  .data-table-module { 
    font-family: Arial, sans-serif; 
    margin: 10px 0; 
  }
  .filter-controls { 
    background-color: #f5f5f5; 
    padding: 10px; 
    margin-bottom: 10px; 
  }
  "
}

Issue 3: Testing Complex Reactive Logic

Problem: Difficulty testing modules with complex reactive dependencies.

Solution:

# Mock reactive dependencies for testing
test_that("module handles complex reactive logic", {
  
  # Create mock reactive data
  mock_data <- reactiveVal(data.frame(
    x = 1:10,
    y = rnorm(10),
    group = sample(c("A", "B"), 10, replace = TRUE)
  ))
  
  # Test with changing data
  testServer(data_table_server, args = list(data = mock_data), {
    
    # Initial state
    session$flushReact()
    initial_data <- session$getReturned()$filtered_data()
    expect_equal(nrow(initial_data), 10)
    
    # Update data and test reaction
    new_data <- data.frame(
      x = 1:5,
      y = rnorm(5),
      group = sample(c("A", "B"), 5, replace = TRUE)
    )
    
    mock_data(new_data)
    session$flushReact()
    
    updated_data <- session$getReturned()$filtered_data()
    expect_equal(nrow(updated_data), 5)
  })
})

Common Questions About Shiny Package Development

The decision depends on your development goals and scale requirements:

Create a Package When:

  • You have reusable modules that will be used across multiple applications
  • Multiple developers or teams need to use the same components
  • You want to distribute your Shiny components to others
  • You need version control and formal documentation for your modules
  • The codebase is large enough that organization and testing become important

Keep in Single Application When:

  • Building a one-off application with application-specific modules
  • Rapid prototyping where formal structure would slow development
  • Small team or individual project where package overhead isn’t justified
  • Components are highly specific to one use case

The general rule is: if you find yourself copying modules between projects or want others to use your components, it’s time to create a package.

Effective dependency management requires careful planning and monitoring:

Version Specification Strategy:

  • Use minimum required versions in DESCRIPTION (e.g., shiny (>= 1.7.0))
  • Avoid overly restrictive upper bounds unless you know of specific incompatibilities
  • Test against multiple R and package versions in CI/CD

Dependency Categories:

  • Imports: Essential packages your code directly uses
  • Suggests: Optional packages for enhanced functionality or examples
  • Depends: Packages that must be attached (rarely used)

Best Practices:

# In DESCRIPTION file
Imports:
    shiny (>= 1.7.0),
    htmltools (>= 0.5.0),
    DT (>= 0.20)
Suggests:
    testthat (>= 3.0.0),
    knitr,
    rmarkdown

# In your R code - check for optional dependencies
if (requireNamespace("plotly", quietly = TRUE)) {
  # Use plotly functionality
} else {
  # Provide fallback or informative message
  warning("Install 'plotly' package for enhanced visualization features")
}

Regular dependency monitoring and testing help catch breaking changes early.

Comprehensive documentation should serve both developers using your modules and those maintaining the code:

Function Documentation (roxygen2):

#' Advanced Data Table Module
#'
#' @description 
#' Creates an interactive data table with filtering, sorting, and export capabilities.
#' Supports both reactive and static data sources.
#'
#' @param id Character string, unique module identifier
#' @param data Reactive data frame or static data frame to display
#' @param options List of additional DT options (optional)
#' @param show_filters Logical, whether to display filter controls (default: TRUE)
#'
#' @return For server function: List containing filtered_data and selected_rows reactives
#'
#' @examples
#' if (interactive()) {
#'   library(shiny)
#'   
#'   ui <- fluidPage(
#'     data_table_ui("example")
#'   )
#'   
#'   server <- function(input, output, session) {
#'     result <- data_table_server("example", reactive(mtcars))
#'     
#'     # Access filtered data
#'     observe({
#'       filtered <- result$filtered_data()
#'       print(paste("Filtered rows:", nrow(filtered)))
#'     })
#'   }
#'   
#'   shinyApp(ui, server)
#' }
#'
#' @export

Vignettes for Complex Workflows:

Create comprehensive tutorials showing real applications, module combinations, and integration with other packages.

README and Website:

  • Quick start guide with minimal working example
  • Installation instructions for different scenarios
  • Link to comprehensive documentation and examples
  • Troubleshooting section for common issues

The key is layered documentation: quick reference for experienced users, detailed tutorials for learners, and technical details for maintainers.

Deployment reliability requires testing across environments and robust error handling:

Environment Testing:

# Create test environments
test_environments <- list(
  local = list(r_version = "4.3.0", os = "local"),
  ci = list(r_version = c("4.1.0", "4.2.0", "4.3.0"), os = c("ubuntu", "windows", "macos")),
  production = list(r_version = "4.2.0", os = "ubuntu")
)

# Test asset loading across environments
test_asset_loading <- function() {
  pkg_name <- utils::packageName()
  
  # Test different asset locations
  locations <- c(
    system.file("www", package = pkg_name),
    file.path("inst", "www"),
    "www"
  )
  
  for (location in locations) {
    if (dir.exists(location)) {
      message("Assets found at: ", location)
      return(TRUE)
    }
  }
  
  warning("No assets found in expected locations")
  return(FALSE)
}

Resource Management:

  • Use system.file() for package assets rather than relative paths
  • Implement fallback mechanisms when resources aren’t found
  • Test asset loading in both development and installed package contexts

Configuration Management:

  • Use environment variables for deployment-specific settings
  • Provide sensible defaults that work across environments
  • Include environment detection utilities in your package

Regular testing in realistic deployment scenarios prevents surprises when your package is used in production environments.

Managing breaking changes requires careful planning and clear communication:

Deprecation Strategy:

# Gradual deprecation approach
old_function <- function(...) {
  .Deprecated("new_function", package = "myShinyPackage")
  new_function(...)
}

# Provide clear migration path
#' Legacy Function (Deprecated)
#'
#' @description 
#' This function is deprecated. Use \code{new_function()} instead.
#'
#' @param ... Arguments passed to new_function
#'
#' @return Same as new_function
#'
#' @examples
#' # Old way (deprecated)
#' # result <- old_function(data)
#'
#' # New way (recommended)
#' result <- new_function(data)
#'
#' @export

Semantic Versioning:

  • Major version (1.0.0 → 2.0.0): Breaking changes
  • Minor version (1.0.0 → 1.1.0): New features, backward compatible
  • Patch version (1.0.0 → 1.0.1): Bug fixes, backward compatible

Change Documentation:

# NEWS.md
## myShinyPackage 2.0.0

### Breaking Changes
- `old_function()` removed (deprecated since v1.5.0)
- Parameter `old_param` renamed to `new_param` in `main_function()`

### Migration Guide
- Replace `old_function(x)` with `new_function(x)`
- Update `main_function(old_param = value)` to `main_function(new_param = value)`

### New Features
- Added `advanced_module()` with enhanced filtering capabilities
- Improved performance for large datasets

Testing Strategy:

  • Maintain tests for deprecated functions during transition period
  • Test migration paths to ensure smooth upgrades
  • Provide example code showing before/after usage patterns

The key is giving users time to adapt while providing clear guidance on how to update their code.

Test Your Understanding

You’re designing a Shiny package that will include data visualization modules, user authentication components, and utility functions. How should you organize the package structure for maximum maintainability and usability?

  1. Put all functions in a single R file and all assets in one directory
  2. Organize by functionality: separate files for modules, authentication, utilities, and organized asset directories
  3. Organize by UI/Server: separate all UI functions from server functions
  4. Create separate sub-packages for each major component
  • Consider how other developers will use and maintain your package
  • Think about logical grouping and discoverability
  • Remember the balance between organization and complexity
  • Consider standard R package conventions

B) Organize by functionality: separate files for modules, authentication, utilities, and organized asset directories

This approach provides the best balance of organization and usability:

Functional Organization Benefits:

  • Related functions are grouped together, making them easier to find and maintain
  • Developers can quickly locate authentication, visualization, or utility functions
  • Each file has a clear, single responsibility
  • Testing and documentation are easier to organize

Recommended Structure:

R/
├── data-modules.R        # Data visualization modules
├── auth-modules.R        # Authentication components  
├── utility-functions.R   # Helper and utility functions
├── asset-management.R    # Asset loading and dependencies
├── theming.R             # Theme configuration
└── package.R             # Package documentation

inst/
├── www/
│   ├── css/             # Organized by asset type
│   ├── js/
│   └── img/
└── examples/            # Example applications

This structure follows R package conventions while providing clear organization for complex Shiny components.

Complete this code to implement proper communication between two modules using shared reactive state:

# Shared state setup
shared_state <- reactiveValues(
  data = NULL,
  filters = ______,
  events = reactiveValues(data_updated = 0)
)

# Module A updates shared data
moduleA_server <- function(id, shared_state) {
  moduleServer(id, function(input, output, session) {
    
    observeEvent(input$update_data, {
      new_data <- process_data()
      shared_state$______ <- new_data
      shared_state$events$______ <- shared_state$events$______ + 1
    })
  })
}

# Module B reacts to shared data changes
moduleB_server <- function(id, shared_state) {
  moduleServer(id, function(input, output, session) {
    
    # React to data updates
    observeEvent(shared_state$events$______, {
      # Update module B when data changes
      update_display(shared_state$______)
    })
  })
}
  • What type of object should store multiple filter values?
  • Which shared_state property should Module A update with new data?
  • Which event should Module A increment when data is updated?
  • Which event should Module B observe to detect data changes?
# Shared state setup
shared_state <- reactiveValues(
  data = NULL,
  filters = list(),
  events = reactiveValues(data_updated = 0)
)

# Module A updates shared data
moduleA_server <- function(id, shared_state) {
  moduleServer(id, function(input, output, session) {
    
    observeEvent(input$update_data, {
      new_data <- process_data()
      shared_state$data <- new_data
      shared_state$events$data_updated <- shared_state$events$data_updated + 1
    })
  })
}

# Module B reacts to shared data changes
moduleB_server <- function(id, shared_state) {
  moduleServer(id, function(input, output, session) {
    
    # React to data updates
    observeEvent(shared_state$events$data_updated, {
      # Update module B when data changes
      update_display(shared_state$data)
    })
  })
}

Key concepts:

  • list() stores multiple named filter values that can be updated independently
  • shared_state$data is the property that stores the actual data shared between modules
  • data_updated is the event counter that gets incremented when data changes
  • Modules observe the event counter rather than the data directly for better performance and clearer dependency tracking

Your Shiny package is ready for distribution. You want to make it available to your organization internally first, then eventually submit to CRAN. What’s the best distribution strategy sequence?

  1. Submit directly to CRAN for maximum visibility
  2. GitHub → Internal testing → CRAN submission → Package website
  3. Private repository → GitHub → Testing feedback → CRAN preparation → Submission
  4. Internal distribution → Public GitHub → Community feedback → CRAN submission → Maintenance
  • Consider the importance of testing and feedback before wide distribution
  • Think about building community and getting input before CRAN submission
  • Remember that CRAN submission is more permanent and has quality requirements
  • Consider ongoing maintenance and support needs

D) Internal distribution → Public GitHub → Community feedback → CRAN submission → Maintenance

This sequence provides the most thorough validation and sustainable distribution:

Internal Distribution: Allows your organization to test thoroughly and provide feedback in a controlled environment where you can quickly fix issues.

Public GitHub: Makes the package available to the broader community for testing and feedback while maintaining flexibility to make changes rapidly.

Community Feedback: Enables you to gather diverse use cases, identify edge cases, and improve documentation based on real user experiences.

CRAN Submission: After thorough testing and community validation, submit a stable, well-documented package that meets R community standards.

Maintenance: Establish ongoing processes for updates, bug fixes, and feature additions while maintaining CRAN compliance.

Benefits of this approach: - Reduces risk of submitting buggy code to CRAN - Builds a user community before official release - Ensures package meets real-world needs - Establishes sustainable maintenance workflows - Provides multiple distribution channels for different user needs

Implementation steps:

# 1. Internal distribution
devtools::install_github("yourorg/myShinyPackage", ref = "develop")

# 2. Public GitHub release
usethis::use_github_release()

# 3. Community feedback period (2-3 months)
# Monitor issues, gather feedback, iterate

# 4. CRAN preparation
devtools::check()
devtools::submit_cran()

Conclusion

Creating professional Shiny packages represents the evolution from individual application development to scalable, reusable component libraries that serve entire development ecosystems. The comprehensive package development skills you’ve mastered enable you to build robust, well-documented libraries that can be shared across teams, organizations, and the broader R community while maintaining the highest standards of software engineering quality.

The package architecture, testing frameworks, and distribution strategies you’ve learned provide the foundation for creating Shiny components that are not just functional, but maintainable, extensible, and reliable across different deployment environments. These professional development practices ensure your packages can evolve with changing requirements while providing stable, well-documented interfaces for other developers.

Package development transforms your Shiny expertise into lasting contributions that can benefit countless developers and applications. Whether you’re creating internal company libraries, contributing to open-source projects, or building commercial Shiny solutions, these skills enable you to create software that meets professional standards and provides long-term value to the development community.

Next Steps

Based on your comprehensive package development knowledge, here are recommended paths for implementing and advancing your Shiny package development skills:

Immediate Implementation Steps (Complete These First)

Advanced Package Development (Choose Your Focus)

For Distribution and Maintenance:

For Production Deployment:

For Enterprise Development:

Long-term Package Development Goals (2-4 Weeks)

  • Publish your first Shiny package to CRAN or internal package repository
  • Establish automated testing and continuous integration workflows for package development
  • Create comprehensive package documentation websites with pkgdown
  • Build a library of reusable Shiny components that can serve multiple projects and teams
Back to top

Reuse

Citation

BibTeX citation:
@online{kassambara2025,
  author = {Kassambara, Alboukadel},
  title = {Creating {Shiny} {Packages:} {Complete} {Guide} to
    {Professional} {Package} {Development}},
  date = {2025-05-23},
  url = {https://www.datanovia.com/learn/tools/shiny-apps/advanced-concepts/package-development.html},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2025. “Creating Shiny Packages: Complete Guide to Professional Package Development.” May 23, 2025. https://www.datanovia.com/learn/tools/shiny-apps/advanced-concepts/package-development.html.