flowchart LR
A["UI<br/>selectInput(inputId = 'mySelection')"] -->|"input$mySelection"| B["SERVER<br/>renderPrint({ ... })"]
B -->|"output$myOutput"| C["UI<br/>verbatimTextOutput(outputId = 'myOutput')"]
style A fill:#2b6c8e,color:#fff
style C fill:#2b6c8e,color:#fff
style B fill:#8fd36f,color:#fff
How Shiny thinks
Inside the UI and the server
The Shiny page showed the logic: the UI sets the options and displays the results, the server elaborates the options and sends back the results. Here we look inside the two pieces, and at how they talk to each other.
The UI is a web page
fluidPage(), selectInput() & co. are R functions that write HTML for you. Try this in the console:
library(shiny)
selectInput(inputId = "mySelection", label = "Select box",
choices = c("Choice 1" = 1, "Choice 2" = 2))R prints (shortened):
<div class="form-group shiny-input-container">
<label class="control-label" for="mySelection">Select box</label>
<select id="mySelection">
<option value="1" selected>Choice 1</option>
<option value="2">Choice 2</option>
</select>
</div>The UI contains two kinds of elements:
- Inputs (
*Input(),actionButton()…): the controls the user plays with - Outputs (
*Output()): empty boxes, each with a name, waiting for the server to fill them
Each element is identified by its ID (id="mySelection"): This IS is the only thing the server knows about the UI.
The server is a function
server <- function(input, output, session) {
...
}Shiny calls this function once for each user that opens the app (each user has their own session). Inside it:
inputis a list with the current value of every input, accessed by ID:input$mySelection. You can read it, you cannot change itoutputis the list of the boxes to fill, accessed by ID:output$myOutput <- render…({ … })
The contract: IDs
Let’s go back to the first app. Use it once more, then we follow the IDs in the code:
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 220
## file: ui.R
library(shiny)
ui <- fluidPage(
selectInput(
inputId = "mySelection", # name used by the server
label = h3("Select box"), # what the user reads
choices = list("Choice 1" = 1, "Choice 2" = 2, "Choice 3" = 3),
selected = 1
),
verbatimTextOutput(outputId = "myOutput") # empty box, filled by the server
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
output$myOutput <- renderPrint({
paste("This is my choice:", input$mySelection)
})
}
# ui.R
ui <- fluidPage(
1 selectInput(inputId = "mySelection",
2 label = h3("Select box"),
3 choices = list("Choice 1" = 1, "Choice 2" = 2, "Choice 3" = 3),
4 selected = 1),
5 verbatimTextOutput(outputId = "myOutput")
)- 1
-
The ID of the input: for internal use, the server will call it
input$mySelection - 2
- The label: for external use, it is what the user reads
- 3
-
The options: the user sees
"Choice 1", the server receives"1"(as text!) - 4
- The option selected when the app starts
- 5
-
The ID of an empty output box: the server will fill
output$myOutput
# server.R
server <- function(input, output, session) {
1 output$myOutput <- renderPrint({
2 paste("This is my choice:", input$mySelection)
})
}- 1
-
Fill the box
myOutput.renderPrint()is the partner ofverbatimTextOutput() - 2
-
Read the current value of the input
mySelection
The rules of the contract
- Defined in the UI, not used in the server: nothing happens. The widget is there, you can play with it, but nobody is listening
- Used in the server, not defined in the UI (often a typo!): Shiny does not warn you
input$myselection(lowercase s) simply isNULL, so the result is empty or the code failsoutput$myOuptutis computed but never shown, because no box has that name
- IDs must be unique: two elements with the same ID make the app behave unpredictably (outputs may not show at all)
inputis read-only:input$mySelection <- 2is an error
Spot the bug
This app has a bug. Try it first: change the choice in the menu and look at the output.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [viewer]
#| viewerHeight: 220
## file: ui.R
library(shiny)
ui <- fluidPage(
selectInput(
inputId = "mySelection",
label = h3("Select box"),
choices = list("Choice 1" = 1, "Choice 2" = 2, "Choice 3" = 3),
selected = 1
),
verbatimTextOutput(outputId = "myOutput")
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
output$myOutput <- renderPrint({
paste("This is my choice:", input$myselection)
})
}
The output never shows the choice. Here is the code: find the bug and fix it in the editor, then press ▶.
#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
#| components: [editor, viewer]
#| layout: vertical
#| viewerHeight: 220
## file: ui.R
library(shiny)
ui <- fluidPage(
selectInput(
inputId = "mySelection",
label = h3("Select box"),
choices = list("Choice 1" = 1, "Choice 2" = 2, "Choice 3" = 3),
selected = 1
),
verbatimTextOutput(outputId = "myOutput")
)
## file: server.R
library(shiny)
server <- function(input, output, session) {
output$myOutput <- renderPrint({
paste("This is my choice:", input$myselection)
})
}
input$myselection must be input$mySelection: IDs are case-sensitive. Since input$myselection does not exist, it is NULL, and paste() silently ignores it.
Reactivity: the mental model
The server contains no loop, no “when the user clicks, do this”. How does the output change when the input changes?
Think of a spreadsheet. If cell B1 contains =A1 * 2, you never “run” B1: every time A1 changes, B1 updates by itself.
render*() functions work the same way:
- the code inside
render*({ ... })is a recipe, not a command executed once - Shiny notes which inputs the recipe reads (here:
input$mySelection) - when one of those inputs changes, Shiny re-runs the recipe and sends the new result to the box
flowchart LR
I["input$mySelection<br/>changes"] --> R["renderPrint({ ... })<br/>runs again"] --> O["output$myOutput<br/>is updated"]
style I fill:#2b6c8e,color:#fff
style R fill:#8fd36f,color:#fff
style O fill:#2b6c8e,color:#fff
This is why the server is not read from top to bottom like a script: each recipe runs when it needs to.
Reading an input outside a recipe does not work:
server <- function(input, output, session) {
choice <- input$mySelection # ❌ outside render*()
output$myOutput <- renderPrint({ choice })
}Error: Can't access reactive value 'mySelection' outside of reactive consumer.
The server function runs once, at the start: at that moment nobody has chosen anything yet. Inputs must be read inside render*() (or the other reactive functions we will meet: reactive(), eventReactive()).
- UI = what the user sees: inputs (controls) and outputs (empty boxes)
- Server = a function of
inputandoutputthat fills the boxes - They talk only through IDs:
inputId→input$id,outputId→output$id render*()are recipes that re-run automatically when the inputs they read change