Breaking changes
-
OpenTelemetry
reactive_updatespans now follow each session's cycle: one span per session, from when the session turns busy until all of its effects (including async ones) have finished, carrying the session'ssession.id. The session's calc, effect, and output spans are its children. Previously, there was one span per reactive flush, which can serve several sessions, so with concurrent effects it couldn't carry a singlesession.idand ended before async effects finished. Effects without a session no longer have areactive_updateparent (#2522). -
Reactive effects now run concurrently. Shiny starts each invalidated effect without waiting for an
asynceffect's awaited part, so a slowasynceffect, calc, or render function no longer delays other sessions. Within a session, input changes andreactive.invalidate_later()still wait until all of that session's effects have finished, and outputs are sent together once they have. What app authors may notice:-
asynceffects interleave at eachawaitinstead of running one after another.priorityorders when effects start, not when they finish. Runs of the same effect still don't overlap: a re-run waits for the previous run. -
reactive.lock()is now defunct and no longer locks the reactive graph. Please remove it from your code. -
await reactive.flush()returns right away, without waiting for dependents, when called from within an effect, areactive.on_flushed()orsession.on_flush()/session.on_flushed()callback, or a task one of these started while it is still running, since waiting there could deadlock. A task that outlives the effect that started it, such as an extended task's body, still waits. The wait covers the whole process, including other sessions' effects and output. -
reactive.on_flushed()callbacks run after each round of effects has started, without waiting forasynceffects, and can run several times perreactive.flush(). Previously, they ran once all effects had finished. An error in one of these callbacks is now printed, and the other callbacks still run, instead of closing a session. -
Message handlers (
session.set_message_handler()) run in their own task, so a slow handler no longer holds up other messages from the client. -
Each session sends its outputs in a task of its own, once all of its effects have finished. A slow client, or a slow
session.on_flush()callback, delays only its own session's output, not other sessions' output or other reactive work. -
Setting a
reactive.valuenow re-runs its dependents automatically, without a call toreactive.flush(). The reactive graph itself still isn't thread-safe, so from another thread useloop.call_soon_threadsafe(value.set, new_value). -
File uploads and
session.dynamic_route()requests no longer mark the session busy. -
Destroying an effect (
effect.destroy()) cancels its in-progress run, raisingasyncio.CancelledErrorat its currentawait. So ending a session, orsession.destroy(id), cancels its in-progressasynceffects and render functions instead of letting them run on after the session or module is gone.
-
-
near_points(add_dist=True), imported fromshiny.plotutils, now names its distance columndist_instead ofdist. Code readingdf["dist"]from the result must now readdf["dist_"](#2510).
Lifecycle changes
-
reactive.lock()is now defunct: it no longer locks the reactive graph and emits aShinyDeprecationWarning. Please remove it from your code (#2520). -
shiny add testis deprecated and now prints a deprecation warning. It will be removed in a future release. Please use theshiny-for-pythonAgent Skill with a coding agent to generate tests instead (#2566).
New features
-
New
session.run_once_when_idle()runs a function once, after the session's effects have finished and its outputs have been sent. Use it to change reactive state from a background task or a custom timer without changing a value that a running effect is still reading (#2546). -
ui.sidebar()gains aroleargument ("form","search","complementary", or"region") for opt-in ARIA landmark markup (rstudio/bslib#1359)."complementary"renders an<aside>, other roles render a<div>with the correspondingroleattribute, and landmark roles require an accessible name (fromtitle,aria_label, oraria_labelledby). Note that the default (role=None) now renders a neutral<div>instead of an<aside>. In addition,ui.page_sidebar()now places the whole sidebar layout inside the page's<main>landmark (#2526).
Bug fixes and minor improvements
-
On Windows, an output's busy indicator now shows while the output renders. Previously, when a synchronous render function blocked the event loop, the message that turns on the indicator reached the browser together with the output's new value, so most outputs never showed it (#1381).
-
UI updates and custom messages sent from outside the reactive graph (e.g. from a background
asynciotask that callsui.update_text()) are now sent to the client right away instead of waiting for the next input change (#21, #1889). -
Jsonifiablenow usesSequenceandMappingfor its container arms instead of the invariantListandDict, so values likedict[str, int]andlist[str]type-check asJsonifiable, including as the return type of a function passed to aRenderer[Jsonifiable]. As a side effect, type checkers also accept other sequences and mappings, such asrange,deque, andbytes, even though onlylist,tuple, anddictserialize. Runtime behavior is unchanged (@wolfgang-aura, #2511). -
near_points(add_dist=True), imported fromshiny.plotutils, now adds thedist_column its documentation describes, instead of a column nameddist. The trailing underscore keeps it from colliding with adistcolumn in your own data. Code readingdf["dist"]from the result must now readdf["dist_"](#2510).near_points()andbrushed_points()also now map a non-string categorical column (e.g. one of integers) to its category codes, instead of raisingValueError: to_float: unsupported dtype for x(#2505). -
@output_args()in Express now accepts every argument of the output's UI function, with matching types.@render.uinow takesinline,container,fill,fillable, and HTML attributes, instead of raising aTypeError.@render.textnow takescontainer.@render.plotand@render.imagenow declareinline,click,dblclick,hover,brush, andfillexplicitly (andwidthandheightfor@render.image), instead of accepting any keyword.@render.download_buttonand@render.download_linknow takelabel(#2539). -
Progress.set(0)now places the bar at0relative tominandmax, instead of sending0unnormalized. Previously, with a negativemin, such asProgress(min=-10, max=10), a value of0showed an empty bar rather than a half-full one (#2518). -
@render.data_framenow serializes PolarsCategoricalandEnumcolumns with Polars >= 2.0, instead of raisingAttributeRemovedError: get_categories was removed in version 2.0(#2529). -
@render.data_framenow labels key-valueMapcolumns (e.g. a pyarrowmap<string, int64>column) asobject, like struct, list, and array columns. This requires a narwhals version that has theMapdtype (#2569). -
@render.data_framenow sends values of pandas nullable dtypes (Int64,Float64,boolean) to the browser as numbers and booleans, instead of strings such as"1"and"True". It also serializes data frames faster and with less memory: a 100,000-row frame with mixed column types serializes 2-3.5x faster, with a 15-40% lower memory peak. The largest gains are for pandas datetime and timedelta columns, duration columns, andDecimalor missing (pd.NA) values. Editing a cell no longer copies the whole data frame (@FBruzzesi, #2572). -
@render.data_frame'sselection_modenow accepts a list (e.g.["rows"]), as its type hint says, instead of raisingTypeError: unhashable type: 'list', and aset_patches_fn()that returns badly shaped patches now raises the intendedValueErrordescribing the expected shape (#2560). -
@render.download_buttonand@render.download_linkgain aniconargument, and pass any other keyword arguments (e.g.class_="btn-success") to the control they auto-place in Express, so an Express app no longer needsui.hold()and a hand-placedui.download_button()just to add an icon. The same arguments can also be given with@output_args()(@raashish1601, #2533). Setting a reactive value from a@render.download_buttondownload handler now updates the outputs and effects that read it right away, including while a streamed download is still sending, instead of only after the next message from the client (#1785). -
req(False, cancel_output=True)inside a@reactive.effectnow stops the effect silently, instead of being treated as an unhandled error that closes the session.SilentCancelOutputExceptionnow subclassesSilentException(#2560). -
session.bookmark.on_bookmark()callbacks in a module now have their saved files found on restore. Restore now looks in<dir>/<ns>, where the files are saved, instead of<dir>/<ns>-(#2560). -
session.on_flush()andsession.on_flushed()callbacks registered while the session's flush callbacks run (by one of them, or by a task that ran while anasyncone was waiting) now run with one more output flush right after, instead of waiting for the next input change. The same goes for a message one of them queues, such as aui.update_*()in anon_flushedcallback. That output flush starts 10 ms later, so a callback that causes one every time it runs (e.g. aonce=Falsecallback that callsui.update_*()) repeats at most 100 times a second (#2580). -
shiny createnow offers the "Stream markdown with enterprise LLM" templates, which were previously never found because of a misspelled path (#2560). -
In
ui.input_date(),datesdisablednow works whenformatis not the defaultyyyy-mm-dd. The dates are now converted on the client the same waymin/maxare, instead of being parsed by bootstrap-datepicker with the displayformat. Thedata-date-dates-disabledattribute is replaced bydata-dates-disabled(and omitted whendatesdisabledisNone), andcontroller.InputDate.expect_datesdisabled()checks the new attribute. This relies on the updated vendoredshiny.js(rstudio/shiny#4434, #2523). -
ui.navset_tab()and the other navsets now addaria-controls, pointing at the panel'sid, to nav links that target a tab panel, alongside the existinghref(rstudio/bslib#1355, #2526).