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
Key Takeaways
- 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.
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
::create_package("~/myShinyPackage")
usethis
# Set up package structure for Shiny development
::use_mit_license()
usethis::use_roxygen_md()
usethis::use_testthat()
usethis::use_package_doc()
usethis::use_readme_md() usethis
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
: myShinyPackage
Package: Package
Type: Professional Shiny Components and Utilities
Title: 0.1.0
Version: Your Name <your.email@example.com>
Author: Your Name <your.email@example.com>
Maintainer: A collection of reusable Shiny modules and utilities
Descriptionfor building professional interactive web applications.
Includes data visualization components, input controls,
and theming utilities.: MIT + file LICENSE
License: UTF-8
Encoding: true
LazyData: 7.2.3
RoxygenNote: knitr
VignetteBuilder:
DependsR (>= 4.1.0)
:
Importsshiny (>= 1.7.0),
htmltools (>= 0.5.0),
bslib (>= 0.4.0),
DT (>= 0.20)
:
Suggeststestthat (>= 3.0.0),
knitr (>= 1.33),
rmarkdown (>= 2.11)
: https://github.com/yourusername/myShinyPackage
URL: https://github.com/yourusername/myShinyPackage/issues BugReports
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)
#' }
<- function(id, height = "400px", show_filters = TRUE) {
data_table_ui <- shiny::NS(id)
ns
::tagList(
shiny# Include package CSS
include_package_css(),
::div(
shinyclass = "data-table-module",
# Filter controls
if (show_filters) {
::div(
shinyclass = "filter-controls",
::fluidRow(
shiny::column(6,
shiny::selectInput(
shinyns("filter_column"),
"Filter Column:",
choices = NULL,
width = "100%"
)
),::column(6,
shiny::textInput(
shinyns("filter_value"),
"Filter Value:",
width = "100%"
)
)
),::div(
shinyclass = "filter-actions",
::actionButton(ns("apply_filter"), "Apply Filter", class = "btn-primary"),
shiny::actionButton(ns("reset_filter"), "Reset", class = "btn-secondary")
shiny
)
)
},
# Data table
::div(
shinyclass = "table-container",
::dataTableOutput(ns("table"), height = height)
DT
)
)
)
}
#' 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
<- function(id, data, options = list()) {
data_table_server ::moduleServer(id, function(input, output, session) {
shiny
# Reactive values for module state
<- shiny::reactiveValues(
values filtered_data = NULL,
selected_rows = NULL
)
# Update column choices when data changes
::observe({
shiny::req(data())
shiny
<- names(data())
column_choices ::updateSelectInput(
shiny
session, "filter_column",
choices = column_choices,
selected = column_choices[1]
)
})
# Apply filter logic
<- shiny::reactive({
filtered_data <- data()
df ::req(df)
shiny
if (!is.null(input$filter_column) &&
!is.null(input$filter_value) &&
$filter_value != "") {
input
<- input$filter_column
filter_col <- input$filter_value
filter_val
if (filter_col %in% names(df)) {
if (is.character(df[[filter_col]])) {
<- df[grepl(filter_val, df[[filter_col]], ignore.case = TRUE), ]
df else if (is.numeric(df[[filter_col]])) {
} <- suppressWarnings(as.numeric(filter_val))
numeric_val if (!is.na(numeric_val)) {
<- df[df[[filter_col]] == numeric_val, ]
df
}
}
}
}
$filtered_data <- df
valuesreturn(df)
})
# Reset filter handler
::observeEvent(input$reset_filter, {
shiny::updateTextInput(session, "filter_value", value = "")
shiny
})
# Render data table
$table <- DT::renderDataTable({
outputfiltered_data()
options = c(
}, list(
pageLength = 25,
scrollX = TRUE,
dom = 'Bfrtip',
buttons = c('copy', 'csv', 'excel'),
responsive = TRUE
),
options
))
# Track selected rows
::observe({
shiny$selected_rows <- input$table_rows_selected
values
})
# 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
<- function() {
include_package_css # Add resource path for package assets
<- utils::packageName()
pkg_name <- system.file("www", package = pkg_name)
resource_path
if (dir.exists(resource_path)) {
::addResourcePath(pkg_name, resource_path)
shiny
::tags$head(
htmltools::tags$link(
htmltoolsrel = "stylesheet",
type = "text/css",
href = paste0(pkg_name, "/css/package-styles.css")
)
)else {
} # Fallback inline styles if package assets not found
::tags$style(
htmltoolstype = "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
<- function() {
create_package_dependencies <- utils::packageName()
pkg_name <- utils::packageVersion(pkg_name)
pkg_version
::htmlDependency(
htmltoolsname = 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", {
<- data_table_ui("test")
ui
# Test that UI is a valid shiny.tag.list
expect_s3_class(ui, "shiny.tag.list")
# Convert to character for content testing
<- as.character(ui)
ui_html
# 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
<- data_table_ui("test", show_filters = TRUE)
ui_with_filters <- as.character(ui_with_filters)
ui_html_with expect_true(grepl("filter-controls", ui_html_with))
# Without filters
<- data_table_ui("test", show_filters = FALSE)
ui_without_filters <- as.character(ui_without_filters)
ui_html_without expect_false(grepl("filter-controls", ui_html_without))
})
test_that("data_table_server handles data correctly", {
# Create test data
<- reactive({
test_data 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
$flushReact()
sessionexpect_equal(nrow(values$filtered_data), 3)
# Test column choices update
expect_equal(
$getReturned()$filtered_data(),
sessiontest_data()
)
# Test filtering
$setInputs(
sessionfilter_column = "city",
filter_value = "New"
)$flushReact()
session
<- session$getReturned()$filtered_data()
filtered_result expect_equal(nrow(filtered_result), 1)
expect_equal(filtered_result$name[1], "Alice")
})
})
test_that("asset functions work correctly", {
# Test CSS inclusion
<- include_package_css()
css_output expect_s3_class(css_output, "shiny.tag.list")
# Test dependency creation
<- create_package_dependencies()
deps 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
<- function() {
test_app <- shiny::fluidPage(
ui data_table_ui("test")
)
<- function(input, output, session) {
server <- reactive({
test_data data.frame(
id = 1:5,
value = rnorm(5),
category = sample(c("A", "B"), 5, replace = TRUE)
)
})
data_table_server("test", test_data)
}
::shinyApp(ui, server)
shiny
}
# Test with shinytest2 (if available)
skip_if_not_installed("shinytest2")
<- shinytest2::AppDriver$new(test_app())
app
# Test initial load
$wait_for_idle()
appexpect_true(app$get_value(output = "test-table") != "")
# Test filtering
$set_inputs(`test-filter_column` = "category")
app$set_inputs(`test-filter_value` = "A")
app$click("test-apply_filter")
app$wait_for_idle()
app
# Verify filter was applied
<- app$get_value(output = "test-table")
table_data expect_true(!is.null(table_data))
$stop()
app })
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
<- function(type = "patch", dev = FALSE) {
update_package_version
# Read DESCRIPTION file
<- "DESCRIPTION"
desc_path if (!file.exists(desc_path)) {
stop("DESCRIPTION file not found")
}
<- readLines(desc_path)
desc_content <- grep("^Version:", desc_content)
version_line
if (length(version_line) == 0) {
stop("Version field not found in DESCRIPTION")
}
<- gsub("Version:\\s*", "", desc_content[version_line])
current_version <- as.numeric(strsplit(current_version, "\\.")[[1]][1:3])
version_parts
# Increment version
switch(type,
"major" = {
1] <- version_parts[1] + 1
version_parts[2:3] <- 0
version_parts[
},"minor" = {
2] <- version_parts[2] + 1
version_parts[3] <- 0
version_parts[
},"patch" = {
3] <- version_parts[3] + 1
version_parts[
}
)
<- paste(version_parts, collapse = ".")
new_version if (dev) new_version <- paste0(new_version, ".9000")
# Update DESCRIPTION
<- paste("Version:", new_version)
desc_content[version_line] 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
::check()
devtools
# 2. Run additional checks
::check_win_devel() # Windows check
devtools::check_for_cran() # Multiple platform check
rhub
# 3. Update documentation
::document()
devtools::build_site()
pkgdown
# 4. Final preparations
::spell_check() # Check spelling
devtools::url_check() # Check URLs urlchecker
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
<- function(base_id, instance = NULL) {
create_unique_module_id
if (is.null(instance)) {
# Generate unique instance identifier
<- format(Sys.time(), "%Y%m%d_%H%M%S")
instance
}
<- paste(base_id, instance, sep = "_")
unique_id return(unique_id)
}
# Usage in application
<- fluidPage(
ui 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
<- function() {
safe_include_assets
<- utils::packageName()
pkg_name
# Try to load package assets
tryCatch({
<- system.file("www", package = pkg_name)
resource_path
if (dir.exists(resource_path)) {
::addResourcePath(pkg_name, resource_path)
shiny
return(htmltools::tags$head(
::tags$link(
htmltoolsrel = "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()
))
}
<- function() {
get_fallback_css "
.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
<- reactiveVal(data.frame(
mock_data 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
$flushReact()
session<- session$getReturned()$filtered_data()
initial_data expect_equal(nrow(initial_data), 10)
# Update data and test reaction
<- data.frame(
new_data x = 1:5,
y = rnorm(5),
group = sample(c("A", "B"), 5, replace = TRUE)
)
mock_data(new_data)
$flushReact()
session
<- session$getReturned()$filtered_data()
updated_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
:
Importsshiny (>= 1.7.0),
htmltools (>= 0.5.0),
DT (>= 0.20)
:
Suggeststestthat (>= 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
<- list(
test_environments 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
<- function() {
test_asset_loading <- utils::packageName()
pkg_name
# Test different asset locations
<- c(
locations 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
<- function(...) {
old_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?
- Put all functions in a single R file and all assets in one directory
- Organize by functionality: separate files for modules, authentication, utilities, and organized asset directories
- Organize by UI/Server: separate all UI functions from server functions
- 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
<- reactiveValues(
shared_state data = NULL,
filters = ______,
events = reactiveValues(data_updated = 0)
)
# Module A updates shared data
<- function(id, shared_state) {
moduleA_server moduleServer(id, function(input, output, session) {
observeEvent(input$update_data, {
<- process_data()
new_data $______ <- new_data
shared_state$events$______ <- shared_state$events$______ + 1
shared_state
})
})
}
# Module B reacts to shared data changes
<- function(id, shared_state) {
moduleB_server 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
<- reactiveValues(
shared_state data = NULL,
filters = list(),
events = reactiveValues(data_updated = 0)
)
# Module A updates shared data
<- function(id, shared_state) {
moduleA_server moduleServer(id, function(input, output, session) {
observeEvent(input$update_data, {
<- process_data()
new_data $data <- new_data
shared_state$events$data_updated <- shared_state$events$data_updated + 1
shared_state
})
})
}
# Module B reacts to shared data changes
<- function(id, shared_state) {
moduleB_server 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?
- Submit directly to CRAN for maximum visibility
- GitHub → Internal testing → CRAN submission → Package website
- Private repository → GitHub → Testing feedback → CRAN preparation → Submission
- 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
::install_github("yourorg/myShinyPackage", ref = "develop")
devtools
# 2. Public GitHub release
::use_github_release()
usethis
# 3. Community feedback period (2-3 months)
# Monitor issues, gather feedback, iterate
# 4. CRAN preparation
::check()
devtools::submit_cran() devtools
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)
- Code Organization and Project Structure - Apply advanced organization principles to package development workflows
- Testing and Debugging Strategies - Implement comprehensive testing frameworks for package quality assurance
- Practice Exercise: Create a complete Shiny package with at least three modules, comprehensive documentation, and automated testing
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
Explore More Articles
Here are more articles from the same category to help you dive deeper into the topic.
Reuse
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}
}