shinyreact
React UI infrastructure for Shiny — Python and R
With shinyreact, the Shiny server (Python or R) contains only reactive computation, and the UI is a React client you own. shinyreact is the bridge between the two; it ships zero UI components itself.
One JavaScript bundle backs both languages, so the same React client works identically against an app.py or an app.R server.
The ui.tsx pattern
- The app file bootstraps a static page —
set_react_page()(Python Express) orpage_react()(Python Core, R). - Your React client, conventionally
ui.tsx(compiled towww/ui.js, discovered automatically), renders the UI. - Client and server talk through hooks:
useShinyInputsends values to the server,useShinyOutputValuereceives whatreactive_outputpublishes, anduseShinyMessageHandlerreceivessend_message()pushes.
from shiny.express import input
from shinyreact import reactive_output, set_react_page
set_react_page()
@reactive_output
def greeting():
return {"message": f"Hello, {input.name()}"}library(shiny)
library(shinyreact)
server <- function(input, output, session) {
output$greeting <- reactive_output({
list(message = paste0("Hello, ", input$name))
})
}
shinyApp(page_react(), server)const { React, ReactDOM, useShinyInput, useShinyOutputValue } = window.shinyreact;
function App() {
const [name, setName] = useShinyInput("name", "world");
const greeting = useShinyOutputValue("greeting");
return (
<>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>{greeting?.message}</p>
</>
);
}
// No mount div in the generated page — append one to <body>.
ReactDOM.createRoot(document.body.appendChild(document.createElement("div"))).render(<App />);The power of reactive_output
In traditional Shiny applications, the server owns the markup: renderPlot(), renderUI(), renderReactable() each return a description of UI that the browser swaps into a placeholder. reactive_output returns nonregulated, raw data. To the Shiny server, it is an ordinary Shiny output in every other way, so the reactive graph, req(), reactive.event, caching, and modules all work unchanged, but what travels the wire is a JSON value, and the client decides what to do with it.
That one change in approach buys several advantages at once:
- One output, many components. A single payload can feed a chart, a table, and a caption. There is no
*Output()placeholder per widget, no per-widget binding JS, and no coupling between the server’s return type and a specific renderer. - The server sends facts, the client does presentation. Sorting, formatting, hover state, tab selection, and “show more” toggles happen in React with no round trip. The server recomputes only when the data actually changes.
- No DOM tear-down. React reconciles the new value into the mounted tree. Pair it with
useShinyOutputStatusto dim stale data while the server recomputes instead of flashing a skeleton.
One reactive_output driving three pieces of UI:
from shiny import reactive
from shiny.express import input
from shinyreact import reactive_output, set_react_page
set_react_page()
sales = load_sales() # long-format fact table: date, region, revenue
@reactive.calc
def filtered():
return sales[sales.region.isin(input.regions())]
@reactive_output
def summary():
df = filtered()
by_month = df.groupby(df.date.dt.to_period("M")).revenue.sum()
return {
"n": int(len(df)),
"total": float(df.revenue.sum()),
"series": [{"month": str(m), "revenue": float(v)} for m, v in by_month.items()],
"rows": df.head(50).to_dict(orient="records"),
}library(shiny)
library(shinyreact)
sales <- load_sales() # long-format fact table: date, region, revenue
server <- function(input, output, session) {
filtered <- reactive({
sales[sales$region %in% input$regions, ]
})
output$summary <- reactive_output({
df <- filtered()
by_month <- aggregate(revenue ~ format(date, "%Y-%m"), df, sum)
names(by_month) <- c("month", "revenue")
list(
n = nrow(df),
total = sum(df$revenue),
series = by_month,
rows = head(df, 50)
)
})
}
shinyApp(page_react(), server)const { useShinyInput, useShinyOutputValue, useShinyOutputStatus } = window.shinyreact;
function Dashboard() {
const [regions, setRegions] = useShinyInput("regions", ["East", "West"]);
const summary = useShinyOutputValue("summary");
const status = useShinyOutputStatus("summary");
const [sortKey, setSortKey] = React.useState("date"); // client-only state, no round trip
if (!summary) return <Skeleton />; // only before the first value arrives
const rows = [...summary.rows].sort((a, b) => (a[sortKey] > b[sortKey] ? 1 : -1));
return (
<div className={status === "recalculating" ? "stale" : ""}>
<RegionPicker value={regions} onChange={setRegions} />
<p>
{summary.n.toLocaleString()} sales, {formatCurrency(summary.total)}
</p>
<RevenueChart data={summary.series} />
<SalesTable rows={rows} sortKey={sortKey} onSort={setSortKey} />
</div>
);
}The server never learns that a chart exists. Swap the chart library, add a third view of the same data, or move the table sort to the server later, and the other side does not change.
Installation
shinyreact is pre-release. Install from GitHub:
pip install "git+https://github.com/posit-dev/shinyreact.git"pak::pak("posit-dev/shinyreact/pkg-r")The JS hooks come with the server package — set_react_page() / page_react() serve shinyreact.js, which installs them at window.shinyreact. Apps with a bundler can instead npm install @posit-dev/shinyreact and pass shinyreact_js="client" to the page entry point.
Agent Skills
Both packages ship two Agent Skills so a coding agent can build a shinyreact app without you pasting documentation into it: shinyreact-build-app builds a ui.tsx-pattern app from scratch, and shinyreact-convert-app ports an existing Shiny app to the pattern.
The fastest starter template is the skill itself. With the shinyreact skills installed, paste this into your agent (swap in R if that is your server):
Using the `/shinyreact-build-app` skill, scaffold a new shinyreact app in this
directory with a Python server. Set up the Vite build (src/ui.tsx compiled to
www/ui.js) and a hello-world UI: a text input whose value the server greets by
name. Run the build and start the app.
Reference
| Python | set_react_page(), page_react(), ReactApp, reactive_output, send_message(), … |
| R | page_react(), reactive_output(), send_message(), … |
| JS | useShinyInput, useShinyOutputValue, useShinyMessageHandler, ShinyOutput, … |
| Examples | Runnable apps, from no-build www/ui.js to Vite + HMR |
| Design | Why the pattern looks the way it does |