| name | r-shiny-debugging |
| description | Debug advanced R Shiny apps with a repeatable workflow for reactive loops, stale outputs, dynamic UI, background tasks, maps, tables, plots, module boundaries, and accessibility regressions. |
| argument-hint | ["bug or symptom"] |
| paths | ["**/app.R","**/global.R","**/server.R","**/ui.R","**/modules/**/*.R","**/mod_*.R"] |
R Shiny Debugging
Diagnose Shiny bugs by narrowing the reactive path, inspecting the structure/dependency layer, stabilizing input updates, and reproducing the failure in a module test.
Quick start
- Reproduce the bug with the smallest possible click path and viewport.
- Read
references/debug-patterns.md.
- If the bug follows custom HTML/CSS, a new dashboard shell, templates, or reusable UI dependencies, also read
references/html-css-dashboard-debugging.md.
- If the bug involves keyboard navigation, missing labels, broken focus, custom widgets, semantic HTML, or other accessibility regressions, also read
references/accessibility-semantic-html-debugging.md.
- If the bug involves hover/focus reveal behavior, motion, onboarding interactions, notifications, tooltips/popovers, or modal behavior, also read
references/micro-interactions-motion-debugging.md.
- If the bug involves mobile overflow, reflow, sidebar collapse behavior, fill-layout issues on narrow screens, or responsive card grids, also read
references/responsive-mobile-debugging.md.
- If the app reads large, Parquet-backed, or repeatedly filtered data, also read
references/arrow-parquet-debugging.md.
- If the app reads large local tables through
data.table, tidytable, or dtplyr, also read references/data-table-reactivity-debugging.md.
- If the app is API-backed, JSON-backed, rate-limited, or refreshes through polling, also read
references/api-backed-shiny-debugging.md.
- If the app is database-backed, also read
references/database-backed-shiny-debugging.md.
- If the app is DuckDB-backed, especially over local Parquet, CSV, Arrow, or a file-backed
.duckdb database, also read references/duckdb-shiny-debugging.md.
- If the bug involves
sf geometry layers, CRS mismatches, polygons that disappear, leafletProxy() updates, legends that drift, or map click selection, also read references/leaflet-sf-debugging.md.
- If the bug involves
future_promise(), ExtendedTask, promises, background jobs, or furrr fan-out inside app code, also read references/future-promises-debugging.md.
- If the bug involves advanced ggplot construction,
plotOutput() brushing/clicking, ggplotly(), plotly events, ggiraph, or interactive figure sizing/tooltips, also read references/ggplot-interactive-figure-debugging.md.
- If the bug involves adjusted predictions, contrasts, slopes, survival/cumulative-incidence companions, plot/table disagreement, stale inferential figures, or wrong scale/reference-line semantics, also read
references/interactive-statistical-figure-debugging.md.
- If the bug involves
reactable, DT, linked table/plot views, lost row selection, wrong filtered row counts, or table refresh state, also read references/interactive-table-debugging.md.
- If the bug appears first as a failing
shinytest2 browser test, snapshot drift, AppDriver issue, or CI-only browser regression, also read references/shinytest2-debugging.md.
- If the bug is really slowness, load degradation, or unclear capacity under concurrency, also read
references/performance-and-load-debugging.md.
- Classify the symptom:
- wrong or blank default selections;
- output looks stale after controls change;
- plot/table fails for some datasets only;
- interactive table state, selected rows, or filtered row counts drift away from the visible table;
- a table refresh clears page, expanded rows, or selected rows in surprising ways;
- custom CSS or theme changes fail to appear;
- a utility class or selector hits the wrong HTML node;
- a template, full HTML UI, or custom component breaks widget styling or input/output binding;
- keyboard users cannot reach or operate a control, tab, dialog, or navigation surface;
- labels, descriptions, or validation/status messages are not announced correctly;
- focus indicators disappear or move to the wrong place;
- tooltip/popover/modal/notification behavior feels wrong, inaccessible, or visually noisy;
- reduced-motion preferences are ignored;
- layout/card heights collapse or mobile behavior looks wrong;
- columns do not stack as expected on narrow screens;
- the page or dashboard scrolls horizontally when it should reflow;
- code reruns too often;
- app startup or a filter change freezes only on large data;
- one output changes another output unexpectedly after a shared local table is mutated;
- cached output looks stale after a data refresh;
- API-backed outputs error, poll too often, parse unexpectedly, or drift away from the current source state;
- database-backed outputs error, stall, or return the wrong slice;
- polygons or markers vanish, click selection points to the wrong feature, or legends drift away from visible layers;
- a background task still blocks the session, never returns, or behaves differently by platform;
- module behavior is hard to pin down;
- a
shinytest2 value snapshot or screenshot changed unexpectedly;
- one user is slow, or many users are slow in different ways.
- Patch the smallest layer that can own the state:
- helper for choice/default derivation;
- event-gated reactive;
- output validation;
- CSS/dependency load path or theme layer;
- HTML binding contract, class placement, or tag surgery layer;
- template head/bootstrap hooks;
- Arrow/Parquet data helper or cache key;
data.table copy/reference boundary, key/order contract, or dtplyr materialization boundary;
- API request helper, parse boundary, polling contract, or rate-limit/retry layer;
- geospatial layer prep, CRS/validity helper, or
leafletProxy() update contract;
- background-task boundary, future plan, or worker-queue layer;
- database pool/query/materialization boundary;
- interactive table state/helper or update/proxy contract;
shinytest2 wait/seed/variant/support-file contract;
- profiling or load-test measurement contract;
- module test.
- Add or update
testServer() coverage when the issue sits inside a module.
Workflow
1. Map the failure path
Write the failing chain in one line:
- input(s) or viewport -> helper/reactive/template/dependency/query -> output surface
That usually reveals whether the bug belongs to:
- input hydration,
- reactive invalidation,
- the structure/dependency layer,
- the database/query layer,
- the DuckDB file/concurrency boundary,
- stale cached state,
- or render-time validation.
When data.table or dtplyr is involved, also ask whether the app is mutating a shared object by reference, reordering rows through setkey(), or materializing a lazy_dt() object earlier than intended.
2. Inspect the structure and dependency layer early
For custom HTML/CSS or templated dashboards:
- inspect generated HTML, head content, singletons, and attached dependencies with
htmltools::renderTags() when the shell is suspicious;
- confirm that full-document templates include the required head/bootstrap hooks;
- confirm that full HTML UI still honors the Shiny binding contract for
name, id, and shiny-* classes;
- confirm that the page
lang, landmarks, headings, labels, descriptions, and focus targets still make sense after custom markup changes;
- confirm that a custom widget is still using a real native control where possible, or that its keyboard/focus contract is explicit when it truly is custom;
- confirm that durable CSS and JS are exposed through
www/ or a formal dependency;
- use
options(shiny.autoreload = TRUE), shiny::devmode(TRUE), or bs_themer() during local design/debug loops.
3. Stabilize dynamic inputs first
When many controls depend on the same dataset or registry:
- derive choices in one helper;
- freeze affected inputs before grouped updates;
- apply updates together;
- pass
server = TRUE for large selectize choice sets.
4. Find the materialization boundary
When large data or remote data is involved, write down exactly where the app stops being lazy and starts pulling rows into memory. If the freeze happens before a narrow slice exists, the bug is often in an early collect(), a full-file read at startup, a database query being rebuilt too often, or a cache key that is hashing too much data.
5. Gate expensive recalculation intentionally
If the user must click a button to confirm recalculation, bind the value-producing reactive to that event and keep notifications/logging in side-effect observers.
6. Make stale state visible
If the displayed result only reflects the last confirmed fit or build, store the last confirmed signature and compare it to current inputs.
7. Validate before render
If a plot, table, or layout region requires a certain variable mix, dependency, fill setting, or local data frame boundary, check readiness in a pure helper and render only when the status object says the output is valid.
8. Reproduce inside testServer()
Once the failure is understood, capture it in a module test so the same stale-state, hydration, design-layer, or database-boundary bug does not recur.
8b. Triage browser-test and snapshot drift deliberately
- Current
shinytest2 guidance supports value, HTML, JavaScript, and screenshot expectations. Decide which contract actually moved before broadening the fix.
- When
expect_values() fails, inspect exported values and render inputs first. Current robust-testing guidance strongly favors shiny::exportTestValues() as the most stable app-contract layer.
- When
expect_screenshot() fails but exported values remain stable, inspect viewport, theme, CSS, and browser-variant drift before assuming the reactive logic is wrong.
- Current in-depth guidance says
wait_for_idle() and wait_for_value() are the durable stabilization tools. If the failure disappears behind Sys.sleep(), the test usually needs a clearer idle or readiness boundary.
- Current in-depth guidance also says tabbed interfaces need an explicit
id for selected-tab tracking. Missing IDs are a common hidden cause of browser-test drift.
- Current
AppDriver documentation says initialization checks duplicate names by default through check_names = TRUE. Treat duplicate input/output names as a real invalid-HTML bug, not a harmless test annoyance.
- For package-owned apps, current package guidance points to
load_app_support(), local_app_support(), and with_app_support() when app-local support files are part of the contract.
8c. Separate single-session slowness from load degradation
- When one user can reproduce the issue, current
profvis documentation makes sampled profiling the right first pass. Profile the representative slow path before assuming concurrency is the cause.
- When one user is fine but many users are not, current
shinyloadtest documentation gives a better debugging surface: record_session() for the recording contract, shinycannon for concurrent replay, load_runs() for event-level analysis, and shinyloadtest_report() for a repeatable HTML summary.
- Current shinyloadtest limitations documentation says WebSockets are required and recordings are server-dependent. Treat deployment mismatch and missing WebSocket support as first-order debugging hypotheses when load tests behave strangely.
- Team doctrine: keep latency debugging and load debugging as separate branches until evidence shows they converge on the same hot path.
Resources
- Read
references/debug-patterns.md for the evidence-backed symptom-to-fix playbook.
- Read
references/html-css-dashboard-debugging.md when the bug follows custom HTML/CSS, templates, missing styles, layout-fill behavior, or reusable UI dependencies.
- Read
references/accessibility-semantic-html-debugging.md when the bug follows semantic markup changes, keyboard/focus regressions, missing labels, or broken custom-widget behavior.
- Read
references/micro-interactions-motion-debugging.md when the bug follows tooltip/popover/modal/notification changes, hover/focus reveal behavior, or motion settings.
- Read
references/responsive-mobile-debugging.md when the bug only appears on narrow viewports, high zoom, mobile sidebars, or responsive card/column layouts.
- Read
references/arrow-parquet-debugging.md when the bug only appears with large analytical data, Parquet working layers, caching, or ExtendedTask/background work.
- Read
references/data-table-reactivity-debugging.md when the bug follows shared data.table mutation, row-order drift after setkey(), repeated local-table conversion, or dtplyr materialization surprises.
- Read
references/api-backed-shiny-debugging.md when the bug follows API polling, request construction, rate limits, JSON parsing, retry/throttle behavior, or stale API-backed outputs.
- Read
references/database-backed-shiny-debugging.md when the bug follows connection pooling, SQL translation, remote/local boundary mistakes, or writeback transaction behavior.
- Read
references/duckdb-shiny-debugging.md when the bug follows DuckDB file locks, local analytical staging, Arrow-to-DuckDB interop, duckplyr fallback, or multi-process deployment behavior.
- Read
references/leaflet-sf-debugging.md when the bug follows CRS mismatches, invalid geometry, missing polygon layers, leafletProxy() update drift, legend/color drift, or map click selection bugs.
- Read
references/future-promises-debugging.md when the bug follows future_promise(), ExtendedTask, promises queueing, background-task boundaries, or furrr fan-out inside app code.
- Read
references/ggplot-interactive-figure-debugging.md when the bug follows advanced ggplot construction, plot interaction, plotly wrapping, event_data(), ggiraph, or interactive figure sizing.
- Read
references/interactive-statistical-figure-debugging.md when the bug follows adjusted-prediction or contrast figures, linked plot/table disagreement, stale inferential surfaces, wrong scale labels, or semantic drift between the figure and the current controls.
- Read
references/interactive-table-debugging.md when the bug follows reactable, DT, linked table/plot state, selection drift, filtered-row mismatches, or refresh-state surprises.
- Read
references/shinytest2-debugging.md when the bug first appears in a browser test, value snapshot, screenshot snapshot, package-app test harness, or CI-only Shiny regression.
- Read
references/performance-and-load-debugging.md when the bug is really app slowness, under-load degradation, profvis interpretation, shinycannon setup, or shinyloadtest result analysis.