Table of content for chapter 18

Chapter section list

The goal of this chapter is to activate your existing skills about writing functions, showing you some specific cases where using functions can substantially improve the clarity of your app. Once you’ve mastered the ideas in this chapter, the next step is to learn how to write code that requires coordination across the UI and server. That requires modules, which you’ll learn about in Chapter 19.

Resource 18.1 : Resources mentioned throughout the chapter

In this chapter, you’ll learn how writing functions can help to manage complex Shiny code. This tends to have slightly different flavors for UI and server components:

  • In the UI, you have components that are repeated in multiple places with minor variations. Pulling out repeated code into a function reduces duplication (making it easier to update many controls from one place), and can be combined with functional programming techniques to generate many controls at once.
  • In the server, complex reactives are hard to debug because you need to be in the midst of the app. Pulling out a reactive into a separate function, even if that function is only called in one place, makes it substantially easier to debug, because you can experiment with computation independent of reactivity.

Functions have another important role in Shiny apps: they allow you to spread out your app code across multiple files. While you certainly can have one giant app.R file, it’s much easier to manage when spread across multiple files.

18.1 File organisation

Functions can live outside of app.R. There are two places you might put them depending on how big they are:

  • Put large functions (and any smaller helper functions that they need) into their own R/\<function-name\>.R file.
  • You might want to collect smaller, simpler, functions into one place. You could R/utils.R for this, but if they’re primarily used in your ui you might use R/ui.R.

If you’ve made an R package before, you might notice that Shiny uses the same convention for storing files containing functions. And indeed, if you’re making a complicated app, particularly if there are multiple authors, there are substantial advantages to making a full fledged package. If you want to do this, I recommend reading the “Engineering Shiny” book and using the accompanying {golem} package. We’ll touch on packages again when we talk more about testing.

18.2 UI functions

Functions are a powerful tool to reduce duplication in your UI code. Let’s start with a concrete example of some duplicated code. Imagine that you’re creating a bunch of sliders that each need to range from 0 to 1, starting at 0.5, with a 0.1 step. You could do a bunch of copy and paste to generate all the sliders:

```{r}
ui <- fluidRow(
  sliderInput("alpha", "alpha", min = 0, max = 1, value = 0.5, step = 0.1),
  sliderInput("beta",  "beta",  min = 0, max = 1, value = 0.5, step = 0.1),
  sliderInput("gamma", "gamma", min = 0, max = 1, value = 0.5, step = 0.1),
  sliderInput("delta", "delta", min = 0, max = 1, value = 0.5, step = 0.1)
)
```

It is worthwhile to recognize the repeated pattern and extract out a function. That makes the UI code substantially simpler:

```{r}
sliderInput01 <- function(id) {
  sliderInput(id, label = id, min = 0, max = 1, value = 0.5, step = 0.1)
}

ui <- fluidRow(
  sliderInput01("alpha"),
  sliderInput01("beta"),
  sliderInput01("gamma"),
  sliderInput01("delta")
)
```

Here a function helps in two ways:

  • We can give the function a evocative name, making it easier to understand what’s going on when we re-read the code in the future.
  • If we need to change the behavior, we only need to do it in one place. For example, if we decided that we needed a finer resolution for the steps, we only need to write step = 0.01 in one place, not four.

18.2.1 Other applications

Functions can be useful in many other places. Here are a few ideas to get your creative juices flowing:

  • If you’re using a customized dateInput() for your country, pull it out into one place so that you can use consistent arguments. For example, imagine you wanted a date control for Americans to use to select weekdays:
```{r}
usWeekDateInput <- function(inputId, ...) {
  dateInput(inputId, ..., format = "dd M, yy", daysofweekdisabled = c(0, 6))
}
```

Note the use of ...; it means that you can still pass along any other arguments to dateInput().

  • Or maybe you want a radio button that makes it easier to provide icons:
```{r}
iconRadioButtons <- function(inputId, label, choices, selected = NULL) {
  names <- lapply(choices, icon)
  values <- if (!is.null(names(choices))) names(choices) else choices
  radioButtons(inputId,
    label = label,
    choiceNames = names, choiceValues = values, selected = selected
  )
}
```
  • Or if there are multiple selections you reuse in multiple places:
```{r}
stateSelectInput <- function(inputId, ...) {
  selectInput(inputId, ..., choices = state.name)
}
```

If you’re developing a lot of Shiny apps (within your organisation), you can help improve cross-app consistency by putting functions like this in a shared package.

18.2.2 Functional programming

Returning back to our motivating example, you could reduce the code still further if you’re comfortable with functional programming.

```{r}
library(purrr)

vars <- c("alpha", "beta", "gamma", "delta")
sliders <- map(vars, sliderInput01)
ui <- fluidRow(sliders)
```

There are two big ideas here:

  • purrr::map() calls sliderInput01() once for each string stored in vars. It returns a list of sliders.
  • When you pass a list into fluidRow() (or any html container), it automatically unpacks the list so that the elements become the children of the container.

If you would like to learn more about purrr::map() (or its base equivalent, base::lapply()), you might enjoy the Functionals chapter of Advanced R.

18.2.3 UI as data

It’s possible to generalize this idea further if the controls have more than one varying input. First, we create an inline data frame that defines the parameters of each control, using tibble::tribble(). We’re turning UI structure into an explicit data structure.

```{r}
vars <- tibble::tribble(
  ~ id,   ~ min, ~ max,
  "alpha",     0,     1,
  "beta",      0,    10,
  "gamma",    -1,     1,
  "delta",     0,     1,
)
```

Then we create a function where the argument names match the column names:

```{r}
mySliderInput <- function(id, label = id, min = 0, max = 1) {
  sliderInput(id, label, min = min, max = max, value = 0.5, step = 0.1)
}
```

Then finally we use purrr::pmap() to call mySliderInput() once for each row of vars:

```{r}
sliders <- pmap(vars, mySliderInput)
```
Note 18.1: I do not have much experience in functional programming

I have already met functional programming but are still not accustomed to it and use it therefore hardly.

See Section 10.3 for more examples of using these techniques to generate dynamic UI in response to user actions.

18.3 Server functions

Whenever you have a long reactive (say >10 lines) you should consider pulling it out into a separate function that does not use any reactivity. This has two advantages:

  • It is much easier to debug and test your code if you can partition it so that reactivity lives inside of server(), and complex computation lives in your functions.
  • When looking at a reactive expression or output, there’s no way to easily tell exactly what values it depends on, except by carefully reading the code block. A function definition, however, tells you exactly what the inputs are.

The key benefits of a function in the UI tend to be around reducing duplication. The key benefits of functions in a server tend to be around isolation and testing.

18.3.1 Reading uploaded data

Take this server from Section 9.1.3 It contains a moderately complex reactive():

```{r}
server <- function(input, output, session) {
  data <- reactive({
    req(input$file)
    
    ext <- tools::file_ext(input$file$name)
    switch(ext,
      csv = vroom::vroom(input$file$datapath, delim = ","),
      tsv = vroom::vroom(input$file$datapath, delim = "\t"),
      validate("Invalid file; Please upload a .csv or .tsv file")
    )
  })
  
  output$head <- renderTable({
    head(data(), input$n)
  })
}
```

If this was a real app, you should seriously consider extracting out a function specifically for reading uploaded files:

```{r}
load_file <- function(name, path) {
  ext <- tools::file_ext(name)
  switch(ext,
    csv = vroom::vroom(path, delim = ","),
    tsv = vroom::vroom(path, delim = "\t"),
    validate("Invalid file; Please upload a .csv or .tsv file")
  )
}
```

When extracting out such helpers, avoid taking reactives as input or returning outputs. Instead, pass values into arguments and assume the caller will turn the result into a reactive if needed. This isn’t a hard and fast rule; sometimes it will make sense for your functions to input or output reactives. But generally, it’s better to keep the reactive and non-reactive parts of your app as separate as possible. In this case, we still use validate(); that works because outside of Shiny validate() works similarly to stop(). But you should keep req() in the server, because it shouldn’t be the responsibility of the file parsing code to know when it’s run.

Since this is now an independent function, it could live in its own file (R/load_file.R, say), keeping the server() svelte. This helps keep the server function focused on the big picture of reactivity, rather than the smaller details underlying each component.

```{r}
server <- function(input, output, session) {
  data <- reactive({
    req(input$file)
    load_file(input$file$name, input$file$datapath)
  })
  
  output$head <- renderTable({
    head(data(), input$n)
  })
}
```

The other big advantage is that you can play with load_file() at the console, outside of your Shiny app. If you move towards formal testing of your app (see ?sec-chap22), this also makes that code easier to test.

18.3.2 Internal functions

Most of the time you’ll want to make the function completely independent of the server function so that you can put it in a separate file. However, if the function needs to use input, output, or session it may make sense for the function to live inside the server function:

```{r}
server <- function(input, output, session) {
  switch_page <- function(i) {
    updateTabsetPanel(input = "wizard", selected = paste0("page_", i))
  }
  
  observeEvent(input$page_12, switch_page(2))
  observeEvent(input$page_21, switch_page(1))
  observeEvent(input$page_23, switch_page(3))
  observeEvent(input$page_32, switch_page(2))
}
```

This doesn’t make testing or debugging any easier, but it does reduce duplicated code in the server function.

We could of course add session to the arguments of the function. But this feels weird as the function is still fundamentally coupled to this app because it only affects a control named “wizard” with a very specific set of tabs.

18.4 Glossary Entries

#> data frame with 0 columns and 0 rows

Session Info

Session Info

Code
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.1 (2025-06-13)
#>  os       macOS Sequoia 15.5
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Europe/Vienna
#>  date     2025-07-18
#>  pandoc   3.7.0.2 @ /opt/homebrew/bin/ (via rmarkdown)
#>  quarto   1.8.4 @ /usr/local/bin/quarto
#> 
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package     * version    date (UTC) lib source
#>  cli           3.6.5      2025-04-23 [1] CRAN (R 4.5.0)
#>  curl          6.4.0      2025-06-22 [1] CRAN (R 4.5.0)
#>  digest        0.6.37     2024-08-19 [1] CRAN (R 4.5.0)
#>  evaluate      1.0.4      2025-06-18 [1] CRAN (R 4.5.0)
#>  fastmap       1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
#>  glossary    * 1.0.0.9003 2025-06-08 [1] local
#>  htmltools     0.5.8.1    2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets   1.6.4      2023-12-06 [1] CRAN (R 4.5.0)
#>  jsonlite      2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
#>  knitr         1.50       2025-03-16 [1] CRAN (R 4.5.0)
#>  rlang         1.1.6      2025-04-11 [1] CRAN (R 4.5.0)
#>  rmarkdown     2.29       2024-11-04 [1] CRAN (R 4.5.0)
#>  rstudioapi    0.17.1     2024-10-22 [1] CRAN (R 4.5.0)
#>  rversions     2.1.2      2022-08-31 [1] CRAN (R 4.5.0)
#>  sessioninfo   1.2.3      2025-02-05 [1] CRAN (R 4.5.0)
#>  xfun          0.52       2025-04-02 [1] CRAN (R 4.5.0)
#>  xml2          1.3.8      2025-03-14 [1] CRAN (R 4.5.0)
#>  yaml          2.3.10     2024-07-26 [1] CRAN (R 4.5.0)
#> 
#>  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/library
#>  [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#> 
#> ──────────────────────────────────────────────────────────────────────────────