shinyreact 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shinyreact/.agents/skills/shinyreact-build-app/SKILL.md +433 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/bookmarking.md +24 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/debugging.md +29 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/modules.md +118 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/no-build.md +35 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/shiny-outputs.md +74 -0
- shinyreact/.agents/skills/shinyreact-build-app/references/testing.md +188 -0
- shinyreact/.agents/skills/shinyreact-convert-app/SKILL.md +282 -0
- shinyreact/__init__.py +24 -0
- shinyreact/_app.py +205 -0
- shinyreact/_bookmark.py +86 -0
- shinyreact/_dep.py +93 -0
- shinyreact/_dep_discovery.py +76 -0
- shinyreact/_input_handler.py +42 -0
- shinyreact/_page.py +656 -0
- shinyreact/_protocol.py +17 -0
- shinyreact/_reactive_output.py +19 -0
- shinyreact/_send_message.py +40 -0
- shinyreact/playwright.py +204 -0
- shinyreact/www/shinyreact.css +1 -0
- shinyreact/www/shinyreact.js +49 -0
- shinyreact-0.1.0.dist-info/METADATA +114 -0
- shinyreact-0.1.0.dist-info/RECORD +25 -0
- shinyreact-0.1.0.dist-info/WHEEL +4 -0
- shinyreact-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shinyreact-build-app
|
|
3
|
+
description: Build a Shiny app whose UI is a React client (the ui.tsx pattern) using shinyreact for Python or R. Use when the user asks to build, extend, or debug a Shiny app with a React front end, mentions shinyreact, set_react_page(), page_react(), reactive_output, or the useShinyInput / useShinyOutputValue hooks, or wants a Shiny server that returns JSON to a client-owned UI instead of rendering HTML.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Building a shinyreact app
|
|
7
|
+
|
|
8
|
+
shinyreact is a bridge, not a component library. The Shiny server holds
|
|
9
|
+
**only reactive computation** and returns JSON; a React client the app author
|
|
10
|
+
owns holds **all** of the UI. They meet at named inputs and outputs.
|
|
11
|
+
|
|
12
|
+
If the task is porting an *existing* Shiny app to this pattern, use the
|
|
13
|
+
`shinyreact-convert-app` skill instead — it starts by describing the app, which
|
|
14
|
+
is the step that decides whether the port is correct.
|
|
15
|
+
|
|
16
|
+
**This skill covers Python and R together.** shinyreact is one API in two
|
|
17
|
+
languages, so anything unmarked below holds in both; `[py]` and `[r]` mark the
|
|
18
|
+
few places they genuinely differ, and the client half is the same either way.
|
|
19
|
+
Skim past the language you are not using rather than assuming it has no
|
|
20
|
+
counterpart.
|
|
21
|
+
|
|
22
|
+
## The shape of every app
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
app.py / app.R server logic only: reactive_output, reactive.effect, calc
|
|
26
|
+
src/ui.tsx the React client you edit (no-build tier: skip src/)
|
|
27
|
+
www/ui.js what the server serves — a Vite build output, or the
|
|
28
|
+
hand-written client on the no-build tier
|
|
29
|
+
www/ui.css optional, discovered the same way
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
There is no `ui.output_*()` placeholder anywhere. The page function discovers
|
|
33
|
+
`www/ui.js` + `www/ui.css` next to the app and serves them; the client appends
|
|
34
|
+
its own mount container to `<body>`.
|
|
35
|
+
|
|
36
|
+
## Step 1 — pick the tier: build unless you cannot
|
|
37
|
+
|
|
38
|
+
**Default to a real Vite build.** JSX, TypeScript, a component library, and
|
|
39
|
+
`npm` are worth having, and the cost of a `package.json` is a one-time
|
|
40
|
+
`npm install` — trivial if you have a terminal. Hand-writing
|
|
41
|
+
`React.createElement` trees to avoid a build step trades a few seconds of setup
|
|
42
|
+
for an app nobody wants to edit.
|
|
43
|
+
|
|
44
|
+
| Tier | Use when | Entry |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| **Vite** (default) | anything real: JSX, Tailwind, shadcn/ui, npm packages, TypeScript | `src/ui.tsx` → built to `www/ui.js` |
|
|
47
|
+
| **No build** | you cannot run `npm` (no toolchain, locked-down host), or the app must ship as one editable file | write `www/ui.js` directly |
|
|
48
|
+
|
|
49
|
+
Both produce the same thing — a classic script at `www/ui.js` — so the server
|
|
50
|
+
side and every hook below are identical, and you can move between tiers later
|
|
51
|
+
without touching `app.py` / `app.R`.
|
|
52
|
+
|
|
53
|
+
### The Vite tier
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
package.json "build": "vite build", "dev": "vite build --watch"
|
|
57
|
+
vite.config.js
|
|
58
|
+
src/ui.tsx entry: mounts <App/>
|
|
59
|
+
src/App.tsx your components
|
|
60
|
+
www/ui.js BUILD OUTPUT — never edit, and gitignore it
|
|
61
|
+
www/ui.css BUILD OUTPUT
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The build **must** externalize React to the global so the app shares the
|
|
65
|
+
instance that owns the hooks:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
build: {
|
|
69
|
+
outDir: "www", emptyOutDir: false, cssCodeSplit: false,
|
|
70
|
+
lib: { entry: "src/ui.tsx", formats: ["iife"], fileName: () => "ui.js" },
|
|
71
|
+
rollupOptions: {
|
|
72
|
+
external: ["react", "react-dom", "react-dom/client"],
|
|
73
|
+
output: {
|
|
74
|
+
assetFileNames: "ui.[ext]", // Vite lib mode emits style.css otherwise
|
|
75
|
+
globals: {
|
|
76
|
+
react: "window.shinyreact.React",
|
|
77
|
+
"react-dom": "window.shinyreact.ReactDOM",
|
|
78
|
+
"react-dom/client": "window.shinyreact.ReactDOM",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Two React copies is the single most common failure mode**, and it presents as
|
|
86
|
+
"hooks return nothing" rather than as an error. If you skip `external` /
|
|
87
|
+
`globals`, Vite bundles its own React and every hook silently stops working.
|
|
88
|
+
|
|
89
|
+
`src/ui.tsx` is the entry, and it is short — the page has no mount container,
|
|
90
|
+
so the app makes its own:
|
|
91
|
+
|
|
92
|
+
```jsx
|
|
93
|
+
import "@/index.css";
|
|
94
|
+
import App from "@/App";
|
|
95
|
+
|
|
96
|
+
const { React, ReactDOM } = window.shinyreact;
|
|
97
|
+
|
|
98
|
+
const root = ReactDOM.createRoot(
|
|
99
|
+
document.body.appendChild(document.createElement("div")),
|
|
100
|
+
);
|
|
101
|
+
root.render(<App />);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Hooks come off the same global: `const { useShinyInput } = window.shinyreact;`.
|
|
105
|
+
(An npm-tier app that installs `@posit-dev/shinyreact` imports them instead and
|
|
106
|
+
externalizes React the same way; reach for that only when you are publishing a
|
|
107
|
+
component library, not for an app.)
|
|
108
|
+
|
|
109
|
+
Run `npm run build` after every client change, or leave
|
|
110
|
+
`npm run dev` (`vite build --watch`) running. **A stale `www/ui.js` is the
|
|
111
|
+
second-most-common confusion** — the source looks right and the browser
|
|
112
|
+
disagrees.
|
|
113
|
+
|
|
114
|
+
### Install libraries; do not hand-roll UI
|
|
115
|
+
|
|
116
|
+
The second reason to take the build tier is that it gives you npm. **Reach for
|
|
117
|
+
an established library before writing a component**, especially for anything
|
|
118
|
+
with accessibility, keyboard handling, or edge cases — a date picker, a data
|
|
119
|
+
table, a combobox, a chart. Hand-rolled equivalents are the bulk of what goes
|
|
120
|
+
wrong in an agent-built app: they are the code with no upstream tests, no
|
|
121
|
+
issue tracker, and no one else reading it, and every line is one more thing
|
|
122
|
+
the user has to review.
|
|
123
|
+
|
|
124
|
+
| Need | Reach for |
|
|
125
|
+
|---|---|
|
|
126
|
+
| components, theming | **shadcn/ui + Tailwind** — the default; components are copied into your source, so they stay editable |
|
|
127
|
+
| icons | `lucide-react` |
|
|
128
|
+
| charts | `recharts` for ordinary business charts; `plotly.js`/`visx`/`d3` when you need their specifics |
|
|
129
|
+
| tables | `@tanstack/react-table` (headless — pair with shadcn's table) |
|
|
130
|
+
| forms | `react-hook-form` + `zod` |
|
|
131
|
+
| dates | `date-fns` |
|
|
132
|
+
| drag and drop | `@dnd-kit/core` |
|
|
133
|
+
|
|
134
|
+
Two shinyreact-specific caveats:
|
|
135
|
+
|
|
136
|
+
- **Never install `react` or `react-dom` as real dependencies** you bundle.
|
|
137
|
+
They stay `external` and come from `window.shinyreact` — see the config
|
|
138
|
+
above. A library listing React as a *peer* dependency is fine and normal.
|
|
139
|
+
- **A widget that already exists on the Shiny side does not need a React port.**
|
|
140
|
+
A data frame, a plotly figure, a leaflet map: keep the render function and
|
|
141
|
+
host it with `ShinyOutput` (Step 3). Re-implementing it in React is work you
|
|
142
|
+
can simply not do.
|
|
143
|
+
|
|
144
|
+
Write a component from scratch when it is genuinely app-specific — the
|
|
145
|
+
histogram that draws *your* data shape, the layout of *your* dashboard. That is
|
|
146
|
+
the part a library cannot know.
|
|
147
|
+
|
|
148
|
+
### The no-build tier
|
|
149
|
+
|
|
150
|
+
If you truly cannot run `npm`, everything comes off the global and `h` stands
|
|
151
|
+
in for JSX. Nesting `React.createElement` gets unreadable fast, which is the
|
|
152
|
+
reason this is the fallback. See
|
|
153
|
+
[`references/no-build.md`](references/no-build.md).
|
|
154
|
+
|
|
155
|
+
## Step 2 — the page entry point
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
# Python, Express
|
|
159
|
+
from shinyreact import set_react_page
|
|
160
|
+
set_react_page() # discovers www/ui.js + www/ui.css
|
|
161
|
+
|
|
162
|
+
# Python, Core
|
|
163
|
+
from shinyreact import ReactApp
|
|
164
|
+
app = ReactApp(server) # add bookmark_store="url" for bookmarking
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```r
|
|
168
|
+
# R
|
|
169
|
+
ui <- page_react() # discovers www/ui.js + www/ui.css
|
|
170
|
+
shinyApp(ui, server)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- All of them are zero-argument. Do not hand-wire an `HTMLDependency`.
|
|
174
|
+
- An app that owns a complete `index.html` (with a `{{ headContent() }}`
|
|
175
|
+
marker) uses `page_react_html()`; `[py]` `ReactApp` discovers that case too,
|
|
176
|
+
`[r]` pass it to `shinyApp(ui = ...)` yourself.
|
|
177
|
+
- Assets are served mtime-versioned, so an edited `ui.js` is never stale in
|
|
178
|
+
the browser cache.
|
|
179
|
+
- Dependencies of traditional renderers are discovered for you either way, so
|
|
180
|
+
there is nothing to wire. `[py]` `set_react_page()` finds the
|
|
181
|
+
`HTMLDependency` objects and injects them into `<head>`; `[r]` the page is
|
|
182
|
+
rendered before `server()` runs, so they are pushed to the client after the
|
|
183
|
+
flush instead.
|
|
184
|
+
- `[py]` paths resolve against the calling module; `[r]` against the working
|
|
185
|
+
directory. Same zero-config result, different rule if you pass one
|
|
186
|
+
explicitly.
|
|
187
|
+
|
|
188
|
+
## Step 3 — the server returns JSON
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
import shinyreact
|
|
192
|
+
from shiny.express import input
|
|
193
|
+
|
|
194
|
+
@shinyreact.reactive_output
|
|
195
|
+
def dist_data():
|
|
196
|
+
return {"breaks": [...], "counts": [...]} # any JSON-able value
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```r
|
|
200
|
+
output$dist_data <- reactive_output({
|
|
201
|
+
# input$bins is NULL until the client's first message — return NULL, not
|
|
202
|
+
# req(): req()'s silent error still reaches the client console.
|
|
203
|
+
n <- input$bins
|
|
204
|
+
if (is.null(n)) return(NULL)
|
|
205
|
+
list(breaks = I(breaks), counts = I(counts)) # I() keeps length-1 as arrays
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Server-side rules that matter:
|
|
210
|
+
|
|
211
|
+
- **Fact table → one shared reactive → one output per card.** Every input
|
|
212
|
+
filters the shared `[py]` `@reactive.calc` / `[r]` `reactive()`; each card
|
|
213
|
+
aggregates from it. Static pre-aggregated tables produce a dashboard where
|
|
214
|
+
half the filters visibly do nothing.
|
|
215
|
+
- Traditional renderers still work and are readable from
|
|
216
|
+
`useShinyOutputValue` with no placeholder. Reach for `reactive_output` when
|
|
217
|
+
the client draws, and a traditional renderer when the server draws
|
|
218
|
+
(`[py]` `@render.plot` / `[r]` `renderPlot()`, hosted by `ImageOutput`) or a
|
|
219
|
+
widget owns the DOM (`[py]` `@render.data_frame`, `@render_plotly` /
|
|
220
|
+
`[r]` `DT::renderDT()`, `plotly::renderPlotly()`, hosted by `ShinyOutput`).
|
|
221
|
+
- `send_message(session, id, data)` pushes a one-off message to
|
|
222
|
+
`useShinyMessageHandler(id, fn)` — for things that are events, not state.
|
|
223
|
+
|
|
224
|
+
### Hosting a traditional renderer
|
|
225
|
+
|
|
226
|
+
A widget that already works does not need a React port. Keep the render
|
|
227
|
+
function and host it client-side — no `*Output()` placeholder on the server:
|
|
228
|
+
`ShinyOutput` for something that owns its own DOM (data frames, plotly, DT,
|
|
229
|
+
leaflet), `ImageOutput` for a server-drawn image (`[py]` `@render.plot`,
|
|
230
|
+
`[r]` `renderPlot()`). Both are in
|
|
231
|
+
[`references/shiny-outputs.md`](references/shiny-outputs.md), including the
|
|
232
|
+
part you cannot guess: how to spell the element each binding looks for.
|
|
233
|
+
|
|
234
|
+
Reach for `reactive_output` plus your own chart whenever the client *could*
|
|
235
|
+
draw it — you get a real React component instead of a server-rendered PNG.
|
|
236
|
+
|
|
237
|
+
A real Shiny *input* widget can be hosted the same way, through a
|
|
238
|
+
`[py]` `@render.ui` / `[r]` `renderUI()` holder inside
|
|
239
|
+
`<ShinyOutput className="shiny-html-output">` — the recipe and its caveats are
|
|
240
|
+
in the same reference. Reserve it for ports that must look widget-for-widget
|
|
241
|
+
identical; React-owned state is still the default for inputs.
|
|
242
|
+
|
|
243
|
+
## Step 4 — the client reads and writes named channels
|
|
244
|
+
|
|
245
|
+
Everything is on `window.shinyreact` (or imported from `@posit-dev/shinyreact` in
|
|
246
|
+
an npm-tier build).
|
|
247
|
+
|
|
248
|
+
| | Full | Read-only | Write-only |
|
|
249
|
+
|---|---|---|---|
|
|
250
|
+
| **Input** | `useShinyInput(id, default)` → `[value, setValue]` | `useShinyInputValue(id)` | `useSetShinyInput(id, default)` |
|
|
251
|
+
| **Output** | — | `useShinyOutputValue(id, default?)` | — |
|
|
252
|
+
| **Status** | | `useShinyOutputStatus(id)` → `"pending" \| "ready" \| "recalculating" \| "error"` | |
|
|
253
|
+
| **Error** | | `useShinyOutputError(id)` → `{message, call, type} \| null` | |
|
|
254
|
+
|
|
255
|
+
Plus `useShinyInitialized()`, `useShinyBusy()`, `useShinyMessageHandler()`, and
|
|
256
|
+
the components `ImageOutput`, `ShinyOutput`, `ShinyModuleProvider`.
|
|
257
|
+
|
|
258
|
+
**Pick the narrowest hook that fits the call site.** A button that pushes and
|
|
259
|
+
never reads uses `useSetShinyInput`; a card that only displays uses
|
|
260
|
+
`useShinyInputValue` / `useShinyOutputValue`. This makes data-flow direction
|
|
261
|
+
visible and avoids re-renders from channels the component never observes.
|
|
262
|
+
|
|
263
|
+
**Shiny modules** — when the same server code runs more than once on a page,
|
|
264
|
+
wrap each instance in `ShinyModuleProvider` and the hooks inside it namespace
|
|
265
|
+
their ids to match the module server. Components stay written as if they owned
|
|
266
|
+
their ids outright. See [`references/modules.md`](references/modules.md) for the
|
|
267
|
+
resolution rules, the `null` vs. omitted distinction, and the pitfalls.
|
|
268
|
+
|
|
269
|
+
### The patterns worth copying verbatim
|
|
270
|
+
|
|
271
|
+
**Action button** — the Shiny idiom, start at 0 and increment:
|
|
272
|
+
|
|
273
|
+
```js
|
|
274
|
+
const [count, setCount] = useShinyInput("go", 0, { debounceMs: 0, priority: "event" });
|
|
275
|
+
```
|
|
276
|
+
```python
|
|
277
|
+
@shinyreact.reactive_output
|
|
278
|
+
@reactive.event(input.go, ignore_init=True) # ignore the initial 0 from mount
|
|
279
|
+
def response(): ...
|
|
280
|
+
```
|
|
281
|
+
```r
|
|
282
|
+
output$response <- reactive_output({
|
|
283
|
+
# ignore the initial 0 the client sends at mount -- NULL, not req(), for the
|
|
284
|
+
# same reason as above
|
|
285
|
+
if (is.null(input$go) || input$go == 0) return(NULL)
|
|
286
|
+
...
|
|
287
|
+
})
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`debounceMs: 0` so rapid clicks are not coalesced by the 100 ms default;
|
|
291
|
+
`priority: "event"` so Shiny treats it as an event.
|
|
292
|
+
|
|
293
|
+
**Loading vs. recalculating** — never collapse the four statuses into one
|
|
294
|
+
boolean:
|
|
295
|
+
|
|
296
|
+
```jsx
|
|
297
|
+
const data = useShinyOutputValue("foo");
|
|
298
|
+
const status = useShinyOutputStatus("foo");
|
|
299
|
+
|
|
300
|
+
// WRONG: unmounts the chart on every input change, tearing down and rebuilding
|
|
301
|
+
// its DOM — the user sees a skeleton flash between every result.
|
|
302
|
+
if (!data || status !== "ready") return <Skeleton/>;
|
|
303
|
+
|
|
304
|
+
// CORRECT: skeleton only before the FIRST value; afterwards keep the chart
|
|
305
|
+
// mounted and dim it while the server recomputes.
|
|
306
|
+
if (!data) return <Skeleton/>;
|
|
307
|
+
return <Chart className={status === "recalculating" ? "recalculating" : ""} data={data}/>;
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
with `.recalculating { opacity: .6; transition: opacity 200ms }`.
|
|
311
|
+
`"pending"` is the only state where you have no data yet; `"recalculating"`
|
|
312
|
+
means the previous result is still valid, so show it.
|
|
313
|
+
|
|
314
|
+
**Showing the server's error text** — `useShinyOutputError(id)` returns the
|
|
315
|
+
same (sanitized) condition/exception message vanilla Shiny would paint into the
|
|
316
|
+
output element, or `null` when the output is fine:
|
|
317
|
+
|
|
318
|
+
```jsx
|
|
319
|
+
const error = useShinyOutputError("foo");
|
|
320
|
+
if (error) return <div className="shiny-output-error">{error.message}</div>;
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`req()` / `validate()` with no message are silent — they never produce an
|
|
324
|
+
error here, matching Shiny. With `shiny.sanitize.errors` / `sanitize_errors`
|
|
325
|
+
on, the server sends its generic message, so the client never has to decide
|
|
326
|
+
what is safe to show.
|
|
327
|
+
|
|
328
|
+
**Gate the first paint** on `useShinyInitialized()` (`if (!initialized) return
|
|
329
|
+
null`) so the UI does not flash empty defaults during connection setup.
|
|
330
|
+
|
|
331
|
+
**App-wide activity** — `useShinyBusy()` is a boolean that tracks Shiny's
|
|
332
|
+
`shiny:busy` / `shiny:idle` events, i.e. whether the server is working on
|
|
333
|
+
*anything*:
|
|
334
|
+
|
|
335
|
+
```jsx
|
|
336
|
+
const busy = useShinyBusy();
|
|
337
|
+
<div className={busy ? "app busy" : "app"}>…</div> // e.g. a top progress bar
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
It seeds `true` if the page is already busy when the component mounts, so a
|
|
341
|
+
component that appears mid-request is not stuck showing "idle". Use it for one
|
|
342
|
+
global cue — a progress bar, a dimmed toolbar. It is the wrong tool for
|
|
343
|
+
per-card state, because *any* output recomputing makes it `true`: use
|
|
344
|
+
`useShinyOutputStatus(id)` there, as above.
|
|
345
|
+
|
|
346
|
+
**Bookmarking** — opt in on the server (`[py]`
|
|
347
|
+
`ReactApp(server, bookmark_store="url")`, `[r]` `enableBookmarking = "url"`
|
|
348
|
+
with a per-request UI function) and the client needs no code at all:
|
|
349
|
+
restored values seed `useShinyInput` initial values before the first paint.
|
|
350
|
+
See [`references/bookmarking.md`](references/bookmarking.md).
|
|
351
|
+
|
|
352
|
+
**Typed inputs** — `useShinyInput(id, default, { type: "shiny.datetime" })`
|
|
353
|
+
appends `:type` to the wire id and routes through Shiny's input handler, so the
|
|
354
|
+
server sees a real date-time (`[py]` `input.when()` is a `datetime` /
|
|
355
|
+
`[r]` `input$when` is a `POSIXct`) instead of the unix seconds the client sent.
|
|
356
|
+
The id/type pairing is a contract: a later mount that disagrees throws.
|
|
357
|
+
|
|
358
|
+
Untyped values go through shinyreact's own `shinyreact.default` handler, which
|
|
359
|
+
keeps R and Python agreeing about the same JSON: `[]` stays empty rather than
|
|
360
|
+
becoming `NULL`, and `[[1, 2], [3, 4]]` keeps its nesting. The one deliberate
|
|
361
|
+
difference is that `[r]` flattens a scalar array to an atomic vector
|
|
362
|
+
(`[0, 100]` → `c(0, 100)`), because that is what R code wants; `[py]` leaves it
|
|
363
|
+
a list. Use `type: "shinyreact.asis"` for the parsed value untouched.
|
|
364
|
+
|
|
365
|
+
## Known pitfalls
|
|
366
|
+
|
|
367
|
+
- **`defaultValue` is captured on first mount only**, like `useState`. Inline
|
|
368
|
+
`{}` / `[]` are safe — stabilized internally.
|
|
369
|
+
- **Inline arrow handlers are safe** for `useShinyMessageHandler` — stored in a
|
|
370
|
+
ref.
|
|
371
|
+
- **Input ids are global per page.** Two mounts of one id share state; that is
|
|
372
|
+
the feature (a button writes, a card reads), but disagreeing on `type`
|
|
373
|
+
throws and disagreeing on `priority` warns and last-writer-wins.
|
|
374
|
+
- **Inputs arrive asynchronously and independently after mount.** An event
|
|
375
|
+
input with `debounceMs: 0` can reach the server before sibling inputs'
|
|
376
|
+
initial values (which sit on the 100 ms default), so a handler that reads
|
|
377
|
+
them at event time sees `NULL` / `None` on the first flush. Give those
|
|
378
|
+
inputs `debounceMs: 0` too, and keep the initial-0 guard shown above.
|
|
379
|
+
- **Terminology**: this is the `ui.tsx` pattern. Never call it an "SPA".
|
|
380
|
+
|
|
381
|
+
## Verify it
|
|
382
|
+
|
|
383
|
+
Do not stop at "the code is written". Three steps, in order:
|
|
384
|
+
|
|
385
|
+
1. **Factor pure logic out of the app file** — binning, formatting,
|
|
386
|
+
conversions go in a module beside the app, where a test can import them
|
|
387
|
+
without starting a session at all.
|
|
388
|
+
2. **Write down what the app does, in plain English, before the tests.** An
|
|
389
|
+
agent that writes the client and then writes the client's tests is
|
|
390
|
+
agreeing with itself — both encode the same misunderstanding. A
|
|
391
|
+
description a human can falsify at a glance is what breaks that loop.
|
|
392
|
+
3. **Test against that description**, at the cheapest layer that can see the
|
|
393
|
+
behavior. Tests go in `tests/` beside the app and must run **from the app
|
|
394
|
+
directory** — `pytest`, or `[r]` `shiny::runTests()`, which needs the
|
|
395
|
+
`tests/testthat.R` + `tests/testthat/` layout.
|
|
396
|
+
|
|
397
|
+
Both languages drive the reactive graph with no browser, which for a `ui.tsx`
|
|
398
|
+
app is most of the server: `[r]` `shiny::testServer()` (`session$setInputs(bins
|
|
399
|
+
= 9)`, then assert on `output$dist_data`) and `[py]` the built-in
|
|
400
|
+
`local_server` pytest fixture (`local_server.set_inputs(bins=9)`, then
|
|
401
|
+
`local_server.get_output("dist_data")`). Either way the value you assert is the JSON the
|
|
402
|
+
client would have received.
|
|
403
|
+
|
|
404
|
+
[`references/testing.md`](references/testing.md) has the four layers, the test
|
|
405
|
+
layout for each language, `testServer()` / `local_server` for plain and module
|
|
406
|
+
servers — including the input ids that need a `:type` suffix and the event
|
|
407
|
+
inputs that need two `set_inputs` calls — how to mount the real `www/ui.js`
|
|
408
|
+
against a fake Shiny, and the traps that cost time (React ignores raw `change`
|
|
409
|
+
events; debounce coalesces within a tick even at `debounceMs: 0`).
|
|
410
|
+
|
|
411
|
+
## Debugging
|
|
412
|
+
|
|
413
|
+
When something does not work, the four diagnostic hooks
|
|
414
|
+
(`useShinyInitialized`, `useShinyOutputStatus`, `useShinyBusy`,
|
|
415
|
+
`useShinyInputValue`) and a symptom-to-cause table are in
|
|
416
|
+
[`references/debugging.md`](references/debugging.md). The first two entries
|
|
417
|
+
cover most of it: every hook returning nothing means two React copies, and
|
|
418
|
+
unchanged behavior in the browser means a stale `www/ui.js`.
|
|
419
|
+
|
|
420
|
+
## Worked examples
|
|
421
|
+
|
|
422
|
+
[`examples/`](https://github.com/posit-dev/shinyreact/tree/main/examples) is the
|
|
423
|
+
reference, each a runnable app: `01-hello` (no-build, SVG chart), `02-columns`
|
|
424
|
+
(event inputs), `03`/`04` (Vite + shadcn/ui — start here for a real build),
|
|
425
|
+
`06`/`07` (`ShinyOutput` hosting a data frame / plotly widget), `08` (typed
|
|
426
|
+
inputs), `09` (Vite HMR against a running Shiny), `10` (bookmarking).
|
|
427
|
+
|
|
428
|
+
[`01-hello`](https://github.com/posit-dev/shinyreact/tree/main/examples/01-hello)
|
|
429
|
+
and
|
|
430
|
+
[`07-plotly`](https://github.com/posit-dev/shinyreact/tree/main/examples/07-plotly)
|
|
431
|
+
each ship an `app.R` **and** an `app.py` over one shared `www/` client. That is
|
|
432
|
+
a demonstration device, not something to copy — a real app has one server — but
|
|
433
|
+
reading the two side by side shows how little of an app is language-specific.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Bookmarking
|
|
2
|
+
|
|
3
|
+
**Bookmarking** — opt in on the server and the client needs no code at all:
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
# [py] -- ReactApp builds the UI per request, which is what makes restore work
|
|
7
|
+
app = ReactApp(server, bookmark_store="url")
|
|
8
|
+
```
|
|
9
|
+
```r
|
|
10
|
+
# [r] -- Shiny's own bookmarking; the UI must be a function of the request,
|
|
11
|
+
# so each visit gets its own restore context
|
|
12
|
+
shinyApp(ui = function(req) page_react(), server, enableBookmarking = "url")
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Trigger it from an action-button input (`[py]` `await session.bookmark()`,
|
|
16
|
+
`[r]` `session$doBookmark()`), which rewrites the browser URL. On a later visit to that URL the page entry point
|
|
17
|
+
embeds the saved values in the page, and the client seeds `useShinyInput`
|
|
18
|
+
initial values from them **before the first paint** — so a bookmarked link
|
|
19
|
+
renders restored state directly, with no flash of the defaults and no
|
|
20
|
+
`useEffect` to write.
|
|
21
|
+
|
|
22
|
+
Two things follow from that: your `useShinyInput` defaults are only used when
|
|
23
|
+
there is nothing to restore, and **restored values appear in the page source**,
|
|
24
|
+
which is inherent to the mechanism — do not bookmark anything secret.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Debugging a shinyreact app
|
|
2
|
+
|
|
3
|
+
The client is a normal React app, so React DevTools works. What is specific to
|
|
4
|
+
shinyreact is the wire, and these five hooks are the instruments — reach for
|
|
5
|
+
them before adding `console.log`:
|
|
6
|
+
|
|
7
|
+
| Hook | Tells you |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `useShinyInitialized()` | whether the WebSocket handshake finished at all |
|
|
10
|
+
| `useShinyOutputStatus(id)` | `"pending"` / `"ready"` / `"recalculating"` / `"error"` for one output |
|
|
11
|
+
| `useShinyOutputError(id)` | the server's sanitized error message for one output, or `null` |
|
|
12
|
+
| `useShinyBusy()` | whether the server is processing *anything* right now |
|
|
13
|
+
| `useShinyInputValue(id)` | what a channel currently holds, read from any component |
|
|
14
|
+
|
|
15
|
+
Symptoms, in the order they actually come up:
|
|
16
|
+
|
|
17
|
+
| Symptom | Almost always |
|
|
18
|
+
|---|---|
|
|
19
|
+
| every hook returns `undefined` / nothing renders | two React copies — the Vite build is missing `external` + `globals` |
|
|
20
|
+
| the browser shows old behavior | stale `www/ui.js`; re-run the build |
|
|
21
|
+
| a page with no content at all | `www/ui.js` was not found — check it sits beside the app and the server logged no warning |
|
|
22
|
+
| an output stays `"pending"` forever | no server output with that exact id, or the server errored before its first value |
|
|
23
|
+
| the server never sees an input | it is debounced (100 ms), or the id has a `type` on one side and not the other |
|
|
24
|
+
| a click is dropped | the default 100 ms debounce coalesced it — use `debounceMs: 0, priority: "event"` |
|
|
25
|
+
| a widget renders 0×0 or not at all | `ShinyOutput` with the wrong tag/class for that binding, or `ImageOutput` with no CSS size |
|
|
26
|
+
| values reappear after reload | a bookmark restore is seeding initial values from the URL |
|
|
27
|
+
|
|
28
|
+
On the server, `reactive_output` is an ordinary Shiny output — print inside it,
|
|
29
|
+
and its errors surface in the Shiny console exactly as usual.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Shiny modules in a shinyreact app
|
|
2
|
+
|
|
3
|
+
Modules exist so one id can appear many times on a page without collision. In a
|
|
4
|
+
normal Shiny app the UI function calls `ns()` and the server gets namespaced ids
|
|
5
|
+
for free. shinyreact has no UI function, so the client supplies the namespace
|
|
6
|
+
instead — with `ShinyModuleProvider`.
|
|
7
|
+
|
|
8
|
+
**Both sides resolve to the same wire id, and neither needs wiring beyond
|
|
9
|
+
naming the namespace once.**
|
|
10
|
+
|
|
11
|
+
## The whole idea
|
|
12
|
+
|
|
13
|
+
```jsx
|
|
14
|
+
const { ShinyModuleProvider, useShinyInput, useShinyOutputValue } = window.shinyreact;
|
|
15
|
+
|
|
16
|
+
function Card() { // knows nothing about namespaces
|
|
17
|
+
const [bins, setBins] = useShinyInput("bins", 30);
|
|
18
|
+
const data = useShinyOutputValue("dist_data");
|
|
19
|
+
...
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
<ShinyModuleProvider namespace="left"><Card /></ShinyModuleProvider>
|
|
23
|
+
<ShinyModuleProvider namespace="right"><Card /></ShinyModuleProvider>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`Card` is written as if it owned `bins` outright. The provider turns its ids
|
|
27
|
+
into `left-bins` and `right-bins` on the wire, which is exactly what the
|
|
28
|
+
matching module server reads.
|
|
29
|
+
|
|
30
|
+
Server side, nothing is special — write an ordinary Shiny module:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# [py]
|
|
34
|
+
from shiny import module
|
|
35
|
+
|
|
36
|
+
@module.server
|
|
37
|
+
def card_server(input, output, session):
|
|
38
|
+
@shinyreact.reactive_output
|
|
39
|
+
def dist_data():
|
|
40
|
+
return histogram(input.bins())
|
|
41
|
+
|
|
42
|
+
card_server("left")
|
|
43
|
+
card_server("right")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```r
|
|
47
|
+
# [r]
|
|
48
|
+
card_server <- function(id) {
|
|
49
|
+
moduleServer(id, function(input, output, session) {
|
|
50
|
+
output$dist_data <- reactive_output({
|
|
51
|
+
if (is.null(input$bins)) return(NULL)
|
|
52
|
+
histogram(input$bins)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
card_server("left")
|
|
57
|
+
card_server("right")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Inputs, outputs, and custom messages all resolve through Shiny's normal module
|
|
61
|
+
resolution, so `send_message(session, "ping", ...)` inside the module reaches
|
|
62
|
+
the `useShinyMessageHandler("ping", ...)` inside the matching provider.
|
|
63
|
+
|
|
64
|
+
## The namespacing rules
|
|
65
|
+
|
|
66
|
+
Every id-taking hook and component goes through one shared resolver, so the
|
|
67
|
+
behavior is identical for `useShinyInput`, `useShinyInputValue`,
|
|
68
|
+
`useSetShinyInput`, `useShinyOutputValue`, `useShinyOutputStatus`,
|
|
69
|
+
`useShinyOutputError`, `useShinyMessageHandler`, `ShinyOutput`, and `ImageOutput`.
|
|
70
|
+
|
|
71
|
+
Each of them takes an optional `namespace`:
|
|
72
|
+
|
|
73
|
+
| `namespace` | Result |
|
|
74
|
+
|---|---|
|
|
75
|
+
| omitted (`undefined`) | the enclosing `ShinyModuleProvider`'s namespace, or no prefix when there is no provider |
|
|
76
|
+
| `"ns"` | prefix with `ns-`, **ignoring** any enclosing provider |
|
|
77
|
+
| `null` | opt out — use the bare id, even inside a provider |
|
|
78
|
+
| `""` | same as `null` |
|
|
79
|
+
|
|
80
|
+
Three consequences worth knowing before you debug them:
|
|
81
|
+
|
|
82
|
+
- **`null` and `undefined` are not the same thing.** Passing `null` is how you
|
|
83
|
+
say "this id is already fully qualified"; leaving it off is how you say "use
|
|
84
|
+
whatever module I am in". The check is `!== undefined` precisely so a `null`
|
|
85
|
+
cannot silently fall through to the context.
|
|
86
|
+
- **The prefix is a single hyphen**: `` `${namespace}-${id}` ``. So
|
|
87
|
+
`namespace="left"` + `id="bins"` is `left-bins`, matching Shiny's own `ns()`.
|
|
88
|
+
- **Nesting overrides rather than concatenates.** An inner
|
|
89
|
+
`ShinyModuleProvider` wins outright; it does not append to the outer one. For
|
|
90
|
+
a genuinely nested module, pass the combined namespace whole:
|
|
91
|
+
`namespace="outer-inner"`.
|
|
92
|
+
|
|
93
|
+
## When to reach for this
|
|
94
|
+
|
|
95
|
+
Only when the *server* has modules. If you just want two of something on the
|
|
96
|
+
page and the server can tell them apart by id, use two ids — modules are the
|
|
97
|
+
answer to "the same server code runs N times", not to "I have two cards".
|
|
98
|
+
|
|
99
|
+
A common middle case: one shared filter driving several cards. That is not a
|
|
100
|
+
module — it is one input id read by several components (`useShinyInputValue`)
|
|
101
|
+
and several outputs. Reach for a module when each instance needs its **own**
|
|
102
|
+
copy of the server logic.
|
|
103
|
+
|
|
104
|
+
## Pitfalls
|
|
105
|
+
|
|
106
|
+
- **A provider around only part of the subtree.** Every hook that belongs to the
|
|
107
|
+
module instance has to be inside it. A control lifted out to a parent silently
|
|
108
|
+
writes the un-namespaced id, and the module server never sees it.
|
|
109
|
+
- **Namespacing an already-namespaced id.** `ImageOutput` passes `null` for its
|
|
110
|
+
internal clientdata ids for this reason; if you build something similar, do
|
|
111
|
+
the same rather than letting the context double-prefix.
|
|
112
|
+
- **Assuming the client namespace is optional.** The server's `ns()` is not
|
|
113
|
+
optional, so the client's must match it exactly — `left-bins`, not `left.bins`
|
|
114
|
+
or `bins`.
|
|
115
|
+
- **`[r]`** a `session` whose `ns()` is not callable makes `send_message()`
|
|
116
|
+
abort by design. The previous silent fallback delivered messages to an
|
|
117
|
+
un-namespaced id that no in-module handler matched, which is much harder to
|
|
118
|
+
find than an error.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The no-build tier
|
|
2
|
+
|
|
3
|
+
Use this only when you cannot run `npm` — no toolchain, a locked-down host, or
|
|
4
|
+
a requirement that the app ship as one editable file. Otherwise take the Vite
|
|
5
|
+
tier: JSX and npm libraries are worth the one-time install.
|
|
6
|
+
|
|
7
|
+
One file, no `package.json`, no build. Everything comes off the global and `h`
|
|
8
|
+
stands in for JSX:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const { React, ReactDOM, useShinyInput, useShinyOutputValue } = window.shinyreact;
|
|
12
|
+
const h = React.createElement;
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
const [bins, setBins] = useShinyInput("bins", 30);
|
|
16
|
+
const data = useShinyOutputValue("dist_data");
|
|
17
|
+
return h("div", null,
|
|
18
|
+
h("input", {
|
|
19
|
+
type: "range", min: 1, max: 50, value: bins,
|
|
20
|
+
onChange: (e) => setBins(Number(e.target.value)),
|
|
21
|
+
}),
|
|
22
|
+
data ? h(Histogram, { data }) : h("p", null, "Loading…"),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// No mount div in the generated page — create the container and append it.
|
|
27
|
+
// The script is deferred, so document.body is parsed by the time this runs.
|
|
28
|
+
const root = ReactDOM.createRoot(
|
|
29
|
+
document.body.appendChild(document.createElement("div")),
|
|
30
|
+
);
|
|
31
|
+
root.render(h(App));
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Nesting gets unreadable fast, which is the reason to prefer the Vite tier.
|
|
35
|
+
|