Philosophy

Shiny comes with a powerful reactive programming model and a rich set of functions for creating UI widgets or custom HTML structure. These features make it possible to quickly build impressive, interactive applications, but they can also make it harder to test and reuse your code.

To address this issue, we recommend separating the code that depends on Shiny from the logic which can be expressed without it. In our experience, this division is crucial for building robust and maintainable applications. To support this separation, Rhino encourages a specific structure for the R sources of your application:

Logic

Use the logic directory for code which can be expressed without Shiny.

Every Shiny app may have a different end goal, but they all generally contain isolatable sections of code that can expressed as a normal R functions. This could be data manipulation, generating non-interactive plots and graphs, or connecting to an external data source, but outside of definable inputs, it doesn't interact with or rely on Shiny in any way.

Code that relies upon reactivity or UI builder/markup functions can be problematic to test and difficult to reuse. With proper design and understanding of this concept, it is possible to express most of your application logic using plain R functions and data structures (like lists, data frames).

View

The view directory should contain code which describes the user interface of your application and relies upon the reactive capabilities of Shiny. Here is where we will use the functions defined in logic, and where the core app functionality will be defined.

If you are not familiar with Shiny modules, please take the time to read up on the concept. In short, using modules we can isolate paired Shiny UI/Server code, and we prevent overlap of reactivity by wrapping all input/output value names with the ns() function. This allows us to "namespace" the running module and use it multiple times in the same application. This is a very important concept to shortly summarize, but if this is new to you just remember that if you want to reference a UI element in the server, it needs to be namespaced.

A typical module could be structured like this:

box::use(
  shiny[moduleServer, NS, renderText, tagList, textInput, textOutput],
)
box::use(
  app/logic/messages[hello_message],
)

#' @export
ui <- function(id) {
  ns <- NS(id)
  tagList(
    textInput(ns("name"), "Name"),
    textOutput(ns("message"))
  )
}

#' @export
server <- function(id) {
  moduleServer(id, function(input, output, session) {
    output$message <- renderText(hello_message(input$name))
  })
}

Minimal app.R

A Rhino application comes with a minimal app.R:

# Rhino / shinyApp entrypoint. Do not edit.
rhino::app()

It is important that you do not edit this file or use it like a global.R file, and instead write your top-level code in app/main.R. It is also important to note that thanks to the shinyApp string in the comment, RStudio recognizes this file as a Shiny application and displays the "Run" and "Publish" buttons.

This approach gives Rhino full control over the startup processes of your application. Steps performed by rhino::app() include:

  1. Purge box cache, so the app can be reloaded without restarting R session.
  2. Configure logger (log level, log file).
  3. Configure static files.
  4. Load the main module / legacy entrypoint.
  5. Add head tags (favicon, CSS & JS).

It is a fair question to ask if we really need a separate main.R file. Couldn't we just define the top-level ui and server in app.R and pass it to rhino::app() as arguments as we would with a normal shiny::shinyApp() call?

The reasoning behind this structure is to enforce consistent use of the {box} modules throughout the application. A file loaded with box::use() can only load other modules/packages with box::use(). In short, this means that we cannot use the library() or source() functions in our app. This is an important distinction from traditional Shiny structure, where we are simply sourcing app.R when the app is loaded.

As the entire Rhino application is loaded with box::use(app/main), all its sources must be properly structured as box modules.



Appsilon/rhino documentation built on Sept. 27, 2024, 7:01 p.m.