18 Functions

As your app gets bigger, it will get harder and harder to hold all the pieces in your head, making it harder and harder to understand. In turn, this makes it harder to add new features, and harder to find a solution when something goes wrong (i.e. it’s harder to debug). If you don’t take deliberate steps, the development pace of your app will slow, and it will become less and less enjoyable to work on.

In this chapter, you’ll learn how writing functions can help. This tends to have slightly different flavours 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.

I assume that you’re already familiar with the basics of functions55. The goal of this chapter is to activate your existing skills, 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.

18.1 File organisation

Before we go on to talk about exactly how you might use functions in your app, I want to start with one immediate benefit: functions can live outside of app.R. There are two places you might put them depending on how big they are:

  • I recommend putting 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. I often use 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:

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)
)

But I think it’s worthwhile to recognise the repeated pattern and extract out a function. That makes the UI code substantially simpler:

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 behaviour, 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 customised 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:

    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:

    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:

    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.

library(purrr)

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

There are two big ideas here:

  • 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 map() (or its base equivalent, lapply()), you might enjoy the Functionals chapter of Advanced R.

18.2.3 UI as data

It’s possible to generalise 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.

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:

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:

sliders <- pmap(vars, mySliderInput)

Don’t worry if this code looks like gibberish to you: you can continue to use copy and paste. But in the long-run, I’d recommend learning more about functional programming, because it gives you such a wonderful ability to concisely express otherwise long-winded concepts. You can see other examples 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():

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, I’d seriously consider extracting out a function specifically for reading uploaded files:

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, I think it’s better to keep the reactive and non-reactive parts of your app as separate as possible. In this case, I’m still using validate(); that works because outside of Shiny validate() works similarly to stop(). But I keep the 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.

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 Chapter 21), 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:

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.

We could of course add session to the arguments of the function:

switch_page <- function(i) {
  updateTabsetPanel(input = "wizard", selected = paste0("page_", i))
}

server <- function(input, output, session) {
  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))
}

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 Summary

As your apps get bigger, extracting non-reactive functions out of the flow of the app will make your life substantially easier. Functions allow you to reactive and non-reactive code and spread your code out over multiple files. This often makes it much easier to see the big picture shape of your app, and by moving complex logic out of the app into regular R code it makes it much easier to experiment, iterate, and test. When you start extracting out function, it’s likely to feel a bit slow and frustrating, but over time you’ll get faster and faster, and soon it will become a key tool in your toolbox.

This functions in this chapter have one important drawback — they can generate only UI or server components, not both. In the next chapter, you’ll learn how to create Shiny modules, which coordinate UI and server code into a single object.