<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Dull Systems]]></title><description><![CDATA[Dull Systems]]></description><link>https://www.dull.systems</link><image><url>https://substackcdn.com/image/fetch/$s_!gy9U!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8db947df-1b08-4bb6-be40-30da63e173e7_200x200.png</url><title>Dull Systems</title><link>https://www.dull.systems</link></image><generator>Substack</generator><lastBuildDate>Sat, 18 Apr 2026 08:31:03 GMT</lastBuildDate><atom:link href="https://www.dull.systems/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Dull Systems]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[dullsystems@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[dullsystems@substack.com]]></itunes:email><itunes:name><![CDATA[Dull Systems]]></itunes:name></itunes:owner><itunes:author><![CDATA[Dull Systems]]></itunes:author><googleplay:owner><![CDATA[dullsystems@substack.com]]></googleplay:owner><googleplay:email><![CDATA[dullsystems@substack.com]]></googleplay:email><googleplay:author><![CDATA[Dull Systems]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Generalized Shiny observer deduplication]]></title><description><![CDATA[A workaround for shiny issues #825 and #4045]]></description><link>https://www.dull.systems/p/observer-deduplication</link><guid isPermaLink="false">https://www.dull.systems/p/observer-deduplication</guid><dc:creator><![CDATA[Miguel Lechón]]></dc:creator><pubDate>Tue, 14 Apr 2026 15:24:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gy9U!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8db947df-1b08-4bb6-be40-30da63e173e7_200x200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h5>Problem statement and current state of affairs</h5><p>Dynamic creation of Shiny observers can lead to problems, as succinctly put by Winston Chang in <a href="https://github.com/rstudio/shiny/issues/825">Shiny issue #825</a>: </p><blockquote><p>if an observer is created in a reactive or observer, it will stay around forever, and keep responding to its inputs</p></blockquote><p>The issue is also nicely explored by Kyle Husmann <a href="https://www.kylehusmann.com/posts/2025/shiny-dynamic-observers">here</a>.</p><p>One way to solve this problem is to collect the dynamically generated observers and <code>destroy()</code>them before launching their replacements. However, that is a finicky transformation that snowballs in complexity when those observers arise from repeat calls to Shiny modules (as described in <a href="https://github.com/rstudio/shiny/issues/825#issuecomment-496679761">this comment</a> and also in <a href="https://github.com/rstudio/shiny/issues/4045">Shiny issue #4045</a>).</p><h5>A solution that scales</h5><p>We believe the way forward is Chang&#8217;s <a href="https://github.com/rstudio/shiny/issues/825#issue-75758069">original proposal</a> of collecting the observers under a &#8220;reactive subdomain&#8221;. The example code he provided (11 years ago!) does not work anymore, but its spirit can be adapted to modern Shiny releases.</p><p><a href="https://github.com/dull-systems/scaling_shiny/tree/main/02-observer_deduplication">Here&#8217;s the code</a>, but please keep on reading before attempting to understand it.</p><p>The interface of the function that captures observers is simple:</p><p><code>observer_dedup(id, expr, session, verbose) </code></p><p>Here, <code>id</code> is a string that uniquely identifies the observer generator, <code>expr</code> is the expression that will generate the observers, <code>session</code> is the caller&#8217;s Shiny session and <code>verbose</code> is a boolean that enables observer removal diagnostic messages.</p><p>The core of that routine, unsurprisingly, runs <code>expr</code> inside a new reactive domain:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;r&quot;,&quot;nodeId&quot;:&quot;eed8248a-e8e2-4232-931c-a0d3c844bd57&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-r">...
state[["subdomain"]] &lt;- shiny:::createSessionProxy(
  session,
  makeScope = make_scope_that_captures_callbacks,
  onEnded = capture_callbacks,
  end = invoke_and_remove_callbacks
)
    
expr &lt;- substitute(expr)
env &lt;- parent.frame()
result &lt;- shiny::withReactiveDomain(state[["subdomain"]], eval(expr, env))
...</code></pre></div><p>As you can see by the use of the triple colon, the original drawback of using an unexported Shiny function is still present here, although one could argue this is less of a problem today<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>When the <code>observer_dedup</code> is reinvoked with the same <code>id</code>, all the previously captured observers under that <code>id</code> are destroyed.</p><h5>Examples of use</h5><p>Applying this function to <a href="https://github.com/rstudio/shiny/issues/825">Shiny issue #825</a> is as simple as:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;r&quot;,&quot;nodeId&quot;:&quot;6ec9b9d0-86fc-4a92-aed4-c9b485e5d037&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-r">server &lt;- function(input, output, session) {
  observe({
    foo &lt;- input$foo
    
    observer_dedup(                                             # change
      id = 'dummy_id', session = session, verbose = TRUE,       # change
      expr =  observe({
        print(paste0("foo was: ", foo, ". bar: ", input$bar))
      })
    )                                                           # change
  })
}</code></pre></div><p>Kyle Husmann&#8217;s example ( the &#8220;<a href="https://github.com/khusmann/shiny-dynamic-observers/tree/main/src/attempt2_dbg">attempt 2</a>&#8221; version) is similarly solved with:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;r&quot;,&quot;nodeId&quot;:&quot;35aa9e2f-5fed-4b11-b10c-14c0f4c14776&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-r">observeEvent(input$column_select, {
  walk(input$column_select, function(i) {
    close_btn_id &lt;- glue("{i}_close")
    observer_dedup(                                         # change
      id = close_btn_id, session = session, verbose = TRUE, # change
      expr = observeEvent(
        input[[close_btn_id]],
        {
          appendMessage(glue("Closing {i}"))
          updateSelectInput(
            inputId = "column_select",
            selected = discard(input$column_select, \(j) j == i)
          )
        },
        ignoreInit = TRUE
      )
    )                                                       # change
  })
})</code></pre></div><p>And as for <a href="https://github.com/rstudio/shiny/issues/4045">Shiny issue #4045</a>, this tweak does the job<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-2" href="#footnote-2" target="_self">2</a>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;r&quot;,&quot;nodeId&quot;:&quot;f91fe487-731c-45b2-8c22-0445051c7027&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-r">server &lt;- function(input, output, session) {  
 observe({
    lapply(1:input$num, function(i) {
      observer_dedup(                                                 # change
        id = paste0(&#8217;server&#8217;, i), session = session, verbose = TRUE,  # change
        expr = selectInputServer(paste0(&#8221;select&#8221;, i))
      )                                                               # change
    })
  })
  ...
}</code></pre></div><h5>A proposal</h5><p>We could write a wrapper around <code>moduleServer</code> to do automatic observer deduplication, using that function&#8217;s <code>id</code> parameter as the identifier for deduplication. But it would be even better for Shiny itself to provide that behavior, at the cost of one extra reactive domain per module. This would turn #4045 into a non-issue.</p><p>The case for stand-alone calls to <code>observe</code>, <code>observeEvent</code>, etc. is not so clear-cut. They don&#8217;t have an <code>id</code> parameter that can make deduplication transparent. However, if they <em>had</em> such a parameter, solving issue #825 could be as simple as providing <code>id = &#8220;my_id&#8221;</code> to the nested observer. That new parameter would be optional. Observers that omitted it would incur no deduplication cost.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>This technique works unmodified since Shiny version 0.13.0 (2016) and we know of no scalable alternative to it. It <em>feels</em> like this function wants to become public and fill a void in the Shiny API. Or, to put it in less impersonal terms, we feel our use of this private function is <em>justified</em>. We will explore this (possibly misguided) sense of entitlement in a future post by the name of &#8220;<em>Hyrum&#8217;s Usucaption&#8221;</em>.</p></div></div><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-2" href="#footnote-anchor-2" class="footnote-number" contenteditable="false" target="_self">2</a><div class="footnote-content"><p>For this last example, there&#8217;s the unrelated issue of preventing <code>actionButtons</code> from firing when they are recreated. We can do that by appending <code>ignoreInit = TRUE</code> to the end of <code>`bindEvent(input$show_popup, ignoreInit = TRUE)`.</code></p></div></div>]]></content:encoded></item><item><title><![CDATA[Ordered shiny::selectInput]]></title><description><![CDATA[A workaround for shiny issue #1490]]></description><link>https://www.dull.systems/p/ordered-select-input</link><guid isPermaLink="false">https://www.dull.systems/p/ordered-select-input</guid><dc:creator><![CDATA[Miguel Lechón]]></dc:creator><pubDate>Fri, 10 Oct 2025 05:52:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gy9U!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8db947df-1b08-4bb6-be40-30da63e173e7_200x200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The vanilla <code>shiny::selectInput</code> drop-down selector fails to preserve the order of its initial value, as documented <a href="https://github.com/rstudio/shiny/issues/1490">here</a>. That same problem also affects bookmark restoration. And we care about bookmarks<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><p>So, to alleviate this problem, we&#8217;ve written <a href="https://github.com/dull-systems/scaling_shiny/tree/main/01-ordered_select_input">OrderedSelectInput</a>. It&#8217;s a simple drop-in replacement that should allow you to address this issue in your projects by substituting calls to <code>shiny::selectInput</code> with <code>OrderedSelectInput</code>.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>And by &#8220;bookmarks&#8221;, we mean <em>state</em>.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Hot reloading Shiny apps]]></title><description><![CDATA[Fewer clicks and faster iteration]]></description><link>https://www.dull.systems/p/hot-reloading-shiny-apps</link><guid isPermaLink="false">https://www.dull.systems/p/hot-reloading-shiny-apps</guid><dc:creator><![CDATA[Miguel Lechón]]></dc:creator><pubDate>Mon, 22 Sep 2025 05:00:00 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!gy9U!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8db947df-1b08-4bb6-be40-30da63e173e7_200x200.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h4>Goal</h4><p>Faster development of shiny-related R packages through app hot reloading<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a>.</p><h4>Problem</h4><p>We build a Shiny app following <a href="https://mastering-shiny.org/action-workflow.html#seeing-your-changes">this procedure</a>:</p><ol><li><p>Change the app code.</p></li><li><p>Launch it.</p></li><li><p>Interact with the app to bring it to a state that allows us to see the effects of the latest changes.</p></li><li><p>Evaluate the behavior of the app.</p></li><li><p>Close it and go all the way back to step <strong>1</strong> if we&#8217;re not done yet.</p></li></ol><p>This launch-interact-close cycle is <strong>expensive, unnecessary friction</strong>. We could instead:</p><ol><li><p>Launch the app [once].</p></li><li><p>Bring it to a state that allows us to see the effects of the changes we&#8217;re about to make [once].</p></li><li><p>Change the app code.</p></li><li><p>Evaluate the behavior of the app and go back to step <strong>3</strong> if we&#8217;re not done yet.</p></li></ol><h4>Traditional solution</h4><ul><li><p>Use the <a href="https://shiny.posit.co/r/reference/shiny/latest/shinyoptions.html">shiny.autoreload option</a> so that Shiny relaunches the app automatically whenever its code changes.</p></li><li><p>Configure <a href="https://mastering-shiny.org/action-bookmark.html#updating-the-url">automatic bookmarking</a> to preserve the application state across reloads.</p></li></ul><h4>Shortcomings of the traditional solution</h4><ul><li><p>This approach does not work for <em>modular</em> Shiny applications prior to shiny 1.11.0. Thanks to <a href="https://github.com/rstudio/shiny/pull/4184">the efforts of Garrick Aden-Buie</a>, this is no longer an issue.</p></li><li><p>This approach still fails to fully hot-reload applications developed as part of an R package.</p></li><li><p>Manually adapting apps for hot-reloading is cumbersome. Cleaning the reloading-related code for publication is also a chore.</p></li></ul><h4>Proposed solution</h4><p>Launch your unmodified Shiny app through our short and flexible <a href="https://github.com/dull-systems/scaling_shiny/tree/main/00-hot_reload">app hot reloader script</a>.</p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>App reloading that preserves app state.</p></div></div>]]></content:encoded></item><item><title><![CDATA[Scaling Shiny]]></title><description><![CDATA[Beyond dashboards]]></description><link>https://www.dull.systems/p/scaling-shiny</link><guid isPermaLink="false">https://www.dull.systems/p/scaling-shiny</guid><dc:creator><![CDATA[Dull Systems]]></dc:creator><pubDate>Sat, 10 May 2025 18:26:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/65e80fee-179e-4905-9dee-c6b86472d308_416x263.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a growing collection of original techniques that can help you build complex Shiny applications. Subscribe<a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" id="footnote-anchor-1" href="#footnote-1" target="_self">1</a> if you&#8217;re into that.</p><h5>State preservation and management</h5><p style="text-align: right;"><em>Users should be able to walk away from an app and resume their work later</em></p><ul><li><p><a href="https://www.dull.systems/p/ordered-select-input">Fix</a> to make <code>shiny::selectInput</code> honor the bookmarked order of selection.</p></li><li><p>Script to take advantage of properly bookmarked applications to achieve <a href="https://www.dull.systems/p/hot-reloading-shiny-apps">hot reloading</a> even inside packages.</p></li><li><p>Manipulation of server-side bookmarks (planned).</p></li><li><p>Testing bookmarks (planned).</p></li></ul><h5>Evergreen Shiny Gotchas</h5><p style="text-align: right;"><em>Solutions to longstanding issues</em></p><ul><li><p>Dealing with <a href="https://www.dull.systems/p/observer-deduplication">nested observers</a>.</p></li><li><p>Herding Shiny IDs (planned)</p></li></ul><h5>Scalable architecture</h5><p style="text-align: right;"><em>Essays</em></p><ul><li><p>Hyrum&#8217;s usucaption (planned)</p></li><li><p>On not packaging (planned)</p></li></ul><p></p><div class="footnote" data-component-name="FootnoteToDOM"><a id="footnote-1" href="#footnote-anchor-1" class="footnote-number" contenteditable="false" target="_self">1</a><div class="footnote-content"><p>We like our posts and code short and to the point, so we won&#8217;t waste your time. We are fond of escape hatches, so our code will never hold yours hostage.</p><p></p></div></div>]]></content:encoded></item></channel></rss>