Explanation: Application structure
Source:vignettes/explanation/application-structure.Rmd
application-structure.Rmd
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:
-
main.R
: The entry point to your application. -
logic
: Application code independent from Shiny. -
view
: Shiny modules and related code.
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:
- Purge box cache, so the app can be reloaded without restarting R session.
- Configure logger (log level, log file).
- Configure static files.
- Load the main module / legacy entrypoint.
- 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.