@posit-dev/shinyreact

React hooks and components for Shiny. The Shiny server — Python or R — contains only reactive computation, and the UI is a React client you own. This package is the bridge, and it ships zero UI components: no buttons, no layout, no theme. You bring the component library.
One React client works against an app.py or an app.R server unchanged. The full site covers all three packages.
Do you need this package?
Only if you use a bundler. shinyreact apps come in two tiers:
- No build step — the server serves the same runtime as an IIFE and the client reads it off
window.shinyreact. Nothing to install; you can stop reading.
- Bundler (Vite, esbuild, webpack) —
npm install this package and import the hooks. You get types, tree-shaking, a development React with Fast Refresh, and your own dependency graph.
Both tiers are built from the same source and speak the same protocol version.
Installation
npm install @posit-dev/shinyreact react react-dom
React 19 is a peer dependency, resolved by your bundler.
Then tell the page entry point that the client brings its own copy, so the server does not also serve the IIFE:
set_react_page(shinyreact_js="client")
page_react(shinyreact_js = "client")
Skip that and the app still works — the registries are page-scoped, so both copies share one set of inputs, outputs, and message handlers — but the page downloads and parses a second React for nothing. The bundle logs a console.warn when it detects this.
Usage
import { useShinyInput, useShinyOutputValue } from "@posit-dev/shinyreact";
import "@posit-dev/shinyreact/styles";
import { createRoot } from "react-dom/client";
function App() {
const [name, setName] = useShinyInput("name", "world");
const greeting = useShinyOutputValue("greeting");
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<p>{greeting}</p>
</div>
);
}
createRoot(document.body.appendChild(document.createElement("div"))).render(
<App />,
);
useShinyInput writes to input.name() / input$name on the server; useShinyOutputValue reads whatever the matching reactive_output returned:
@reactive_output
def greeting():
return f"Hello, {input.name()}!"
output$greeting <- reactive_output({
paste0("Hello, ", input$name, "!")
})
The "./styles" export is only needed for components that ship CSS (ImageOutput's placeholder spinner); the hooks have no styles of their own.
API
| Inputs | useShinyInput · useShinyInputValue · useSetShinyInput |
| Outputs | useShinyOutputValue · useShinyOutputStatus · useShinyOutputError |
| Messaging | useShinyMessageHandler |
| Session | useShinyInitialized · useShinyBusy |
| Components | ShinyOutput · ImageOutput · ShinyModuleProvider |
| Utilities | MISSING · PROTOCOL_VERSION |
Pick the narrowest hook that fits the call site: a button that pushes events but never reads its own state wants useSetShinyInput, not useShinyInput with a discarded value.
ShinyOutput renders a traditional Shiny output element — a shiny-data-frame, a plotly widget — inside a React tree, and handles binding for you.
Full signatures and types: JS API reference.
Try it
git clone https://github.com/posit-dev/shinyreact
cd shinyreact/examples/11-npm-local
shiny run app.py
Learn more
License
MIT © Posit Software, PBC