

Ms. Rory Chen
Manager of Dementias Platform Australia (DPAU)
Centre for Healthy Brain Ageing (CHeBA)
Discipline of Psychiatry & Mental Health
School of Clinical Medicine, UNSW Medicine & Health
DPAU: https://www.dementiasplatform.com.au
LinkedIn: https://www.linkedin.com/in/rorychenxy

Comfortable with simple operations in R and R Studio
No prior knowledge of Shiny apps required.
Install the {shiny} and {rhino} package if you havenβt already
The workshop will run for 1 hour (recorded).
What is Rhino?
Real-world R Shiny App Demo to inspire your own
Slides + Coding Example + Exercise
Ask questions!
Shiny?
β¨Shiny is a framework for creating interactive web applications using R code.
UI (User Interface): How your app looks
Server: How your app works
Reactivity: User changes input controls β updates output (via server function)
https://rorychenxy.github.io/shiny-workshops/slides/intro-to-shiny.html
Rhino?
π¦Rhino is an Enterprise-grade framework for building Shiny applications
Developed by Appsilon https://appsilon.github.io/rhino/
Opinionated framework with proven industry experience
Modern software engineering practices
Rhino?Clear Code (modules, file structures)
Quality Check
Automation
We will see details later π.
Led: UNSW Sydneyβs Centre for Healthy Brain Ageing (CHeBA)Partnership:
Funding support:


Accelerate the progress in dementia research
Transform the epidemiology of ageing and dementia
Global Ageing and Dementia Study Explorer (GLADSE) https://portal.dementiasplatform.com.au/crs-metadata-explorer
~/gladse
βββ app
β βββ data
β β βββ crsdir_df.fst
β β βββ crslist.fst
β β βββ csurvone_df.fst
β β βββ csurv_filter.fst
β β βββ csurv_matrix.fst
β β βββ filter_df.fst
β β βββ map_filter.rda
β β βββ measurement_inv.fst
β β βββ meta_filter.fst
β β βββ repolist.fst
β β βββ varinfo.fst
β βββ js
β β βββ index.js
β βββ logic
β β βββ footer.R
β β βββ value_map.R
β β βββ vlabel.R
β β βββ __init__.R
β βββ main.R
β βββ static
β β βββ favicon.ico
β β βββ images
β βββ styles
β β βββ main.scss
β βββ view
β βββ connect_map.R
β βββ crs_directory.R
β βββ crs_filters.R
β βββ csurv_matrix.R
β βββ dt_table.R
β βββ map_filter.R
β βββ repo.R
β βββ stmt.R
β βββ __init__.R
βββ app.R
βββ config.yml
βββ dependencies.R
βββ gladse.Rproj
βββ renv
β βββ activate.R
β βββ library
β β βββ windows
β βββ settings.json
β βββ staging
βββ renv.lock
βββ rhino.yml
βββ rsconnect
β βββ shinyapps.io
β βββ dpau
βββ tests
βββ cypress
β βββ e2e
βββ cypress.config.js
βββ testthat
βββ test-main.Rapp/main.R - Application entry pointapp/logic/ - Business logic (Shiny-independent)app/view/ - UI components and modulesapp/static/ - static like picsapp/styles/ - CSS filesCreate your rhino app with the New Project feature with R studio.

rhino::init("first-rhino-app")shiny::devmode().shiny::runApp().app/main.Rbox::use(
shiny[bootstrapPage, div, moduleServer, NS, renderUI, tags, uiOutput],
)
#' @export
ui <- function(id) {
ns <- NS(id)
bootstrapPage(
uiOutput(ns("message"))
)
}
#' @export
server <- function(id) {
moduleServer(id, function(input, output, session) {
output$message <- renderUI({
div(
style = "display: flex; justify-content: center; align-items: center; height: 100vh;",
tags$h1(
tags$a("Hello, World! Check out Rhino docs!",
href = "https://appsilon.github.io/rhino/")
)
)
})
})
}ns <- NS(id) [Step 3.5]NS(id) is used in Shiny modules to create a namespace function that helpsrhino framework, in the UI, the ID must be wrapped in ns(), but in the server the namespace is applied automatically.Try shiny::runExample ("01_hello")
We will convert a traditional shiny app into a modular Rhino app script
Check the traditional shiny app script: 01_hello.R
Complete the modular Rhino app script: 01_hello_rhino.R
Copy the syntax and create your /app/view/hist.R
Rhino? - Clear CodeScalable App Architecture
{box} packagemain.R with the following syntax/app/view/hello.Rmain.R# app/view/hello.R
box::use(
shiny,
)
#' @export
ui <- function(id) {
ns <- shiny$NS(id)
shiny$div(
shiny$h1("Hello, World!"),
## update the inputID with ns()
shiny$textInput(inputId = ?,
label = "Your Name",
value = '',
placeholder = NULL),
## update the outputId with ns()
shiny$textOutput(outputId = ?)
)
}
#' @export
server <- function(id) {
shiny$moduleServer(id, function(input, output, session) {
# inputID & # outputId
output$______ <- shiny::renderText({
paste('Hello, ', input$_____, "!", sep = '')
})
})
}Copy the following syntax: 03_reverse_logic.R
Create your app/logic/reverse.R
Refer to 03_reverse.R
Update your app/view/hello.R
Add new output with reversed name in UI
Update the Server
app/static folderapp/logic/const.Rapp/view/favorites.Rmain.R to call the βfavoriteβ modulebox::use(
# Call the const we defined
)
# Call the favorite module with different options
## UI
favorites$ui(ns("fruits"), category = "fruits", choices = const$fruits),
favorites$ui(ns("vege"), category = "vegetables", choices = const$vegetables),
## Server
favorites$server("fruits")
favorites$server("vege")
{testthat}{box} modulesrhino::init(){renv}{config}Refer to this map filter app 05_mapfilter.R
# Packages -----
library(shiny)
library(leaflet)
library(spData)
library(dplyr)
library(sf)
# Prepare Data -----
data(world)
mapdata <- world |> mutate(country = name_long)
# UI -----
ui <- function(id) {
shiny::fluidPage(
shiny::wellPanel(shiny::actionButton("resetmap", "Reset Map")),
leaflet::leafletOutput("mapfilter", height = 400),
shiny::tags$h4("Selected countries:"),
shiny::verbatimTextOutput("filtered_country")
)
}
# Server -----
server <- function(input, output, session) {
# Define reactive values
rv <- shiny::reactiveValues(selected_countries = NULL, # Initialize reactive value for selected counties
last_click_id = NULL,
filtered_data = mapdata)
shiny::observeEvent(input$mapfilter_shape_click, { # this is the logic behind the "click" of the map.
click <- input$mapfilter_shape_click
rv$last_click_id <- click$id
########## map behavior ################
# If a country is clicked
if (click$id %in% rv$selected_countries) {
# If selected, remove it
rv$selected_countries <- rv$selected_countries[rv$selected_countries != click$id]
} else if(click$id == "selected"){ # when a county is clicked again it is removed
rv$selected_countries <- rv$selected_countries[rv$selected_countries != tail(rv$selected_countries, n = 1)]
}else {
# If not selected, add it
rv$selected_countries <- c(rv$selected_countries, click$id)
}
# Now update the leaflet
leaflet::leafletProxy("mapfilter", session) |>
leaflet::addPolygons(data = mapdata,
layerId = ~country,
label = ~country,
fillColor = ifelse(mapdata$country %in% rv$selected_countries, "#F47A60", "#7fe7dc"), # Change fill color based on selection
col = "#316879",
weight = 2,
fillOpacity = ifelse(mapdata$country %in% rv$selected_countries, 1, 0.5),
highlight = highlightOptions(
fillOpacity = 1,
bringToFront = TRUE)
)
})
output$filtered_country <- shiny::renderPrint({
paste(rv$selected_countries, collapse = ',')
})
# Leaflet
output$mapfilter <- leaflet::renderLeaflet({ # rendering the filter map
leaflet::leaflet() |>
leaflet::addTiles() |> # The is the base map
leaflet::addPolygons(data = mapdata,
color = '#316879',
weight = 1,
layerId = ~country,
label = ~country,
fillColor = "#7fe7dc",
fillOpacity = .5,
highlight = highlightOptions(
fillOpacity = 1,
bringToFront = TRUE
)) |>
leaflet::setView(zoom = 1, lng = 0, lat =50)
})
# Reset the map filter
shiny::observeEvent(input$resetmap, {
rv$selected_countries <- NULL
rv$last_click_id <- NULL
rv$filtered_data <- mapdata
leafletProxy("mapfilter", session) |>
leaflet::addPolygons(data = mapdata,
color = '#316879',
weight = 1,
layerId = ~country,
label = ~country,
fillColor = "#7fe7dc",
fillOpacity = .5,
highlight = highlightOptions(
fillOpacity = 1,
bringToFront = TRUE
)) |>
leaflet::setView(zoom = 1, lng = 0, lat =50)
})
# Return reactive values
return(
list(
value = shiny::reactive(rv$selected_countries),
filtered = shiny::reactive(rv$filtered_data)
)
)
}
# Run the app -----
shinyApp(ui = ui, server = server)Can you convert it into a module?
