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.
@@ -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
+