Skip to content

docs(skills): two silent failure modes of the hosted-widget recipe - #312

Merged
schloerke merged 1 commit into
mainfrom
schloerke/hosted-widget-timing-gotchas
Sep 13, 2026
Merged

schloerke merged 1 commit into
mainfrom
schloerke/hosted-widget-timing-gotchas

Conversation

@schloerke

Copy link
Copy Markdown
Collaborator

What

shiny-outputs.md's hosted-input recipe (#294) teaches the mechanism but not the two ways it silently fails. Both produce no error, no warning, and nothing in the console — the widget just doesn't update, or doesn't appear.

  1. An update_slider() / updateSliderInput() sent before the holder has rendered is dropped. The section previously said updates "keep working against the id" with no qualifier — which is exactly the sentence that walks a reader in. Guidance: put the value in the widget's own constructor call; reach for update_* only for a value you don't know until after the widget exists.
  2. A holder that isn't visible on first paint is suspended, so its render function never runs. [py] suspend_when_hidden=False / [r] suspendWhenHidden = FALSE is the opt-out.

Neither is a shinyreact bug — a plain output_ui() / uiOutput() app with no React anywhere loses the same update and suspends the same output. But the holder recipe makes both easy to walk into, which is why they belong in the skill.

Where this came from

samuelbharti/shinyreact-apps docs/upstream — a gap report from a downstream repo doing pixel-identical bslib→shinyreact ports, plus a draft patch. Their report verified both in R and explicitly flagged the Python side as unverified guesswork. This PR verifies Python.

Four other findings in that report I deliberately left out: useShinyOutputValue's auto-prefixing vs. a user-written namespace helper (the only genuinely shinyreact-specific one, but only bites if you wrap the module hooks yourself); bs_global_theme()'s first arg being version; loadSupport() sourcing R/ before app.R's library() calls; page_navbar() markup varying with getCurrentTheme(). The last three are bslib/Shiny trivia, and the fourth only bites if you hand-write navbar TSX instead of hosting it.

Verification

Two new e2e tests in pkg-py/tests/playwright/test_hosted_input_timing.py, each with a built-in control so neither can pass vacuously:

  • test_update_before_the_holder_renders_is_dropped — startup update_slider(value=30), holder renders on click, slider arrives at 9. Then #bump fires the identical call once the widget exists and it lands at 30. The first assertion fails if the update had been applied; the second fails if updates never work at all.
  • test_hosted_widget_hidden_on_first_paint_never_renders — two identical holders in one display: none panel, differing only by session.output(suspend_when_hidden=False). Only the opted-out one exists in the DOM. Confirmed it fails when that one line is applied to both (i.e. the test triggers unless the gotcha is real).
make py-test-e2e     # 30 passed (was 28)
make py-check        # All checks passed
R testthat           # pass, incl. the skills drift check

make update-skills run; FEATURES.md updated with both leaves marked (e2e) and noted as Shiny's behavior rather than ours.

Not done

R halves of both claims rest on the upstream report's R verification plus the shared Shiny mechanism, not a test here — R has no e2e suite yet (#194).

`shiny-outputs.md`'s hosted-input recipe (#294) leads readers straight into
two Shiny behaviors it does not mention, both of which fail with no error,
no warning, and nothing in the console:

- an `update_slider()` / `updateSliderInput()` sent before the holder has
  rendered targets an id the client has not bound, and is dropped. The
  section previously said updates "keep working against the id" with no
  qualifier, which is exactly the sentence that walks you in.
- a holder that is not visible on first paint is suspended, so its render
  function never runs and no widget appears at all.

Both are plain Shiny — a `output_ui()` / `uiOutput()` app with no React
anywhere loses the same update and suspends the same output — but the
holder recipe makes them easy to hit, so the skill now documents them.

Found via samuelbharti/shinyreact-apps' upstream gap report, which verified
both in R over ~20 real ports. Pinned here for Python with two new e2e
tests, each with its own control so neither can pass vacuously: the update
test bumps the same slider after it exists (9 -> 30), and the suspension
test mounts two identical holders in one hidden panel differing only by
`suspend_when_hidden=False`. Flipping that one line fails the test.

R has no e2e suite yet (#194), so the R halves are the upstream report's
verification plus the shared Shiny mechanism, not a test here.
@schloerke
schloerke merged commit fde8ed9 into main Sep 13, 2026
17 checks passed
@schloerke
schloerke deleted the schloerke/hosted-widget-timing-gotchas branch September 13, 2026 02:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant