Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 48 additions & 14 deletions examples/01-hello/README.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,67 @@
# Example 13 — ui.tsx hello world (no build step)
# Example 01Old Faithful, `ui.tsx` style (no build step)

The smallest possible `ui.tsx`-first Shiny app: a Python server that contains only reactive logic, plus a static React client served from `www/`. No JSX, no bundler, no `package.json`. Edit `app.js` and reload.
Shiny's canonical [`01_hello`](https://github.com/rstudio/shiny/blob/main/inst/examples-shiny/01_hello/app.R)
app — a bins slider over the Old Faithful waiting times — rebuilt as the
smallest possible `ui.tsx`-first app. No JSX, no bundler, no `package.json`.
Edit `app.js` and reload.

`app.py` (Express, via `set_react_page()`) and `app-core.py` (Core, via `page_react_html()`) are two server-side entries for the same `www/` client.
`app.py` (Express, via `set_react_page()`) and `app-core.py` (Core, via
`page_react_html()`) are two server-side entries for the same `www/` client;
`app.R` is the R twin.

## What it shows

A name-input form and click counter rendered twice for direct comparison:
The split that makes the pattern worth using. In traditional Shiny, `01_hello`
renders the histogram *on the server* — `renderPlot({ hist(...) })` ships a PNG
to the browser and the client is a passive `<img>`. Here the server never
produces a picture:

- **Client card** — `Hello, {name}!` and `Count: {clickCount}` computed locally in React state. Updates on every keystroke / click with no roundtrip.
- **Server card** — same two values, but routed through Shiny: `useShinyInput("name")` → `@reactive.calc greeting` → `@reactive_output txtout_title` → `useShinyOutputValue("txtout_title")`. Updates lag by the websocket round-trip.
- **Server** — one `reactive_output` returning `{breaks, counts}` (plain JSON
from R's `hist(..., plot = FALSE)` / a dependency-free Python binner), plus a
caption string. That's the entire server. No plotting library, no image
encoding, no `plotOutput` placeholder.
- **Client** — `www/app.js` reads that JSON with `useShinyOutputValue` and draws
the bars as SVG `<rect>`s. Because the chart is real DOM the client owns, it
can be styled, animated, or made interactive without another round trip.

The point is that the same data shows up on both cards but the latency is visibly different — the client card is instantaneous, the server card has the websocket delay you'd expect.
The bins slider goes the other way: `useShinyInput("bins", 30)` pushes the value
to Shiny, which recomputes the counts.

It also demonstrates the output-status idiom from the repo's guidance — the
chart stays mounted while the server recomputes and only dims via
`useShinyOutputStatus("dist_data") === "recalculating"`. Dragging the slider
never tears the SVG down and re-mounts it.

## Layout

```
examples/01-hello/
├── app.py # Express: set_react_page() + 2 reactive_output outputs
├── app-core.py # Core: page_react_html() + App(app_ui, server), same outputs
├── app.R # R: page_react_html() + reactive_output, same outputs
├── faithful.py # Old Faithful waiting times + a stdlib-only binner (Python)
├── faithful.csv # base R's `faithful` dataset, exported for the Python servers
└── www/
├── index.html # 3 lines: stylesheet, #root div, script
├── app.js # raw React.createElement (with `h` shorthand)
└── main.css # body reset
├── index.html # 2 lines: stylesheet, script (the app appends its own mount div to <body>)
├── app.js # raw React.createElement (with `h` shorthand) + an SVG histogram
└── main.css # sidebar/panel layout
```

Five files. No `node_modules`, no Vite, no build script.
No `node_modules`, no Vite, no build script — and on the Python side no
numpy/matplotlib either.

`app.R` uses base R's built-in `faithful` dataset; `faithful.csv` is that same
data exported so the Python servers don't need a data dependency. R's outputs
return `NULL` until the client's first `bins` message arrives (Python raises a
silent exception instead), and wrap the histogram vectors in `I()` so a
single-bin result still serializes as a JSON array rather than a scalar.

## Bridge primitives used

- `from shinyreact import reactive_output, set_react_page` (Express server, `app.py`) / `page_react_html` (Core server, `app-core.py`)
- `window.shinyreact.useShinyInput(id, default, options?)` for the name field and click counter
- `window.shinyreact.useShinyOutputValue(id, default)` for the server-computed title and count
- `from shinyreact import reactive_output, set_react_page` (Express server, `app.py`) / `page_react_html` (Core server, `app-core.py`); `library(shinyreact)` with `page_react_html()` + `reactive_output()` in `app.R`
- `window.shinyreact.useShinyInput(id, default)` for the bins slider
- `window.shinyreact.useShinyOutputValue(id, default)` for the histogram data and caption
- `window.shinyreact.useShinyOutputStatus(id)` to dim the chart while it recalculates
- `window.shinyreact.useShinyInitialized()` to suppress the placeholder UI during connection setup

`window.shinyreact.React` and `window.shinyreact.ReactDOM` are pulled in directly so the React app shares the React instance that owns the shinyreact hooks.
Expand All @@ -54,3 +84,7 @@ Open the URL printed by Shiny.
## When to use this pattern

Good fit for `ui.tsx`-first apps that are small enough to not need JSX or component libraries — proof of concept, internal tools, anything where the cost of running a build is more than the cost of writing `React.createElement` calls. As soon as you want shadcn or Tailwind utility classes, see [03-columns-shadcn](../03-columns-shadcn/) and [04-shadcn](../04-shadcn/) for the Vite-based setup.

If you'd rather keep rendering server-side, [04-shadcn](../04-shadcn/) shows
`@render.plot` + `ImageOutput` (matplotlib PNGs) side by side with the data-only
approach used here.
17 changes: 7 additions & 10 deletions examples/01-hello/app-core.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
from shiny import App, Inputs, Outputs, Session, reactive
from faithful import histogram, waiting
from shiny import App, Inputs, Outputs, Session
from shinyreact import page_react_html, reactive_output

app_ui = page_react_html() # serves www/index.html (Core API)


def server(input: Inputs, output: Outputs, session: Session):
@reactive.calc
def greeting():
name = input.name()
return name if name else "World"

@reactive_output
def txtout_title():
return f"Hello, {greeting()}!"
def dist_data():
return histogram(waiting, input.bins())

@reactive_output
def txtout_count():
return input.click_count()
def dist_caption():
n = input.bins()
return f"{len(waiting)} eruptions in {n} bin{'' if n == 1 else 's'}"


app = App(app_ui, server)
38 changes: 30 additions & 8 deletions examples/01-hello/app.R
Original file line number Diff line number Diff line change
@@ -1,20 +1,42 @@
library(shiny)
library(shinyreact)

# Base R ships the Old Faithful dataset; the Python servers read the same data
# from the faithful.csv exported next to this file.
waiting <- faithful$waiting

ui <- page_react_html("www/index.html")

server <- function(input, output, session) {
greeting <- reactive({
name <- input$name
if (is.null(name) || nchar(name) == 0) "World" else name
})
# input$bins is NULL until the client's first useShinyInput("bins", 30)
# message arrives. Returning NULL leaves the React side on its "Loading…"
# placeholder; req() would work too, but its silent error still reaches the
# client. (Python's input.bins() raises a silent exception instead.)
bins <- reactive(input$bins)

output$txtout_title <- reactive_output({
paste0("Hello, ", greeting(), "!")
output$dist_data <- reactive_output({
n <- bins()
if (is.null(n)) {
return(NULL)
}
breaks <- seq(min(waiting), max(waiting), length.out = n + 1)
h <- hist(waiting, breaks = breaks, plot = FALSE)
# I() keeps length-1 vectors as JSON arrays (n = 1) instead of scalars.
list(breaks = I(h$breaks), counts = I(h$counts))
})

output$txtout_count <- reactive_output({
input$click_count
output$dist_caption <- reactive_output({
n <- bins()
if (is.null(n)) {
return(NULL)
}
paste0(
length(waiting),
" eruptions in ",
n,
" bin",
if (n == 1) "" else "s"
)
})
}

Expand Down
19 changes: 6 additions & 13 deletions examples/01-hello/app.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
from shiny import reactive
from faithful import histogram, waiting
from shiny.express import input
from shinyreact import reactive_output, set_react_page

set_react_page()


@reactive.calc
def greeting():
name = input.name()
if not name:
return "World"
return name


@reactive_output
def txtout_title():
return f"Hello, {greeting()}!"
def dist_data():
return histogram(waiting, input.bins())


@reactive_output
def txtout_count():
return input.click_count()
def dist_caption():
n = input.bins()
return f"{len(waiting)} eruptions in {n} bin{'' if n == 1 else 's'}"
Loading
Loading