posit-dev / posit-dev/shinyreact
set_react_page: automatically deliver dependencies for renderers registered after page load
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 14
- Forks
- 3
- Avg merge
- 9h 12m
- Merged PRs (30d)
- 74
Description
Follow-up to #87. Builds on the automatic dependency discovery shipped there (synchronously-mounted top-level + @module.server renderers) and the @render.ui dynamic-UI path (delivered natively by Shiny).
Goal
Make the server-registers-a-renderer-after-page-load + React-supplied placeholder pattern work automatically, with no user ceremony. Example:
@reactive.effect
@reactive.event(input.add)
async def _():
@render_plotly
def scatter():
return px.scatter(...)
get_current_session().output(scatter) # registered after the page is built
…with the React client mounting <ShinyOutput id="scatter" .../>. Today the chart never renders: the renderer's HTMLDependency (the binding JS) was never delivered, so the element can't bind.
What we already ruled in / out (investigation summary)
All verified with Playwright spikes against shinywidgets render_plotly:
- Value delivery is NOT the problem. A dynamically-registered output sends its value fine; Shiny's
bindOutputeven replays a stored$values[id]when the element later binds. A dynamically-registeredrender.text(binding JS in coreshiny.js) renders correctly. - The problem is the missing dependency.
dep_scripts=0 → bound=0 → no render. Proof: aui.hold()-ed warm-uprender_plotly(so Layer A injects the binding JS at startup) makes the dynamically registered output render (bound=1, plotly=1). - Naive late-push does NOT work for widgets. Pushing the dep after registration +
renderDependenciesAsync+bindAllfails withError: No model found for id …. shinywidgets sends the widget model via acomm_opencustom message alongside the value; because the dep (which registers the comm handler) loads asynchronously aftercomm_openarrives, the message is dropped and the model is never created. This is a fundamental ordering problem, not a timing tweak (retryingbindAllfor 2.4s did not help — the model is gone). - A re-trigger handshake DOES work (
bound=1, plotly=1): push deps → client loads them → client signals server → the output recomputes → shinywidgets re-sendscomm_openwith its handler now present. - But auto-triggering that recompute is the open problem. The handshake only worked when the renderer depended on a reactive that the deps-ready signal bumped (one line of user cooperation). Two no-cooperation triggers failed:
- re-registering the renderer (
session.output(renderer)again) →bound=0; - manually invalidating the output effect's context (
effect._ctx.invalidate()) →bound=0(invalidation without Shiny's flush-scheduling pipeline doesn't drive the recompute).
- re-registering the renderer (
What "automatic" needs (remaining work)
A way to force a registered output to recompute through Shiny's normal reactive pipeline — without the user's renderer explicitly depending on a deps-ready signal. Candidate directions to explore:
- A supported force-invalidate-and-flush primitive (invalidate the output context and request a session flush), rather than the private
effect._ctxpoke that didn't drive the recompute. - A
suspend_when_hidden-based dance: register the dynamic output suspended/hidden, push deps, then reveal it so Shiny's resume-on-show recomputes it after deps load. - A shinyreact-owned reactive that dynamic outputs implicitly depend on (would likely require wrapping the renderer at registration).
Interim opt-in fallback (proven, ~1 line): have the dynamic renderer depend on a shinyreact-provided "deps-ready" reactive so the existing reactive path recomputes it.
Related
- #87 — automatic dep discovery for synchronously-mounted renderers (Layer A).
- Design/investigation notes:
docs/superpowers/specs/2026-06-11-dynamic-renderer-dep-delivery-design.md.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with docs/superpowers/specs/2026-06-11-dynamic-renderer-dep-delivery-design.md and reproduce the Playwright spikes described in the issue. Trace dynamic output registration, dependency loading, and Shiny's normal reactive flush pipeline, including the shinywidgets custom-message ordering case. Done means a renderer registered after page load renders automatically with a React placeholder, without a user-supplied deps-ready reactive.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, typescript
- Domain
- backend, frontend, full-stack
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100