<!-- mobian-agent-page publisher="dailydev" canonical="https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe" -->

---
title: My first attempt at porting a Shiny app failed, and not...
description: A developer recounts rewriting an internal R Shiny staffing app (StaffPuzzle, 1,200 lines) into htmxr, a server-rendered HTML approach hydrated with htmx and...
canonical: https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe
twitter:card: summary_large_image
twitter:site: @dailydotdev
og:type: website
og:site_name: daily.dev
og:title: My first attempt at porting a Shiny app failed, and not for a technical reason | daily.dev
og:description: A developer recounts rewriting an internal R Shiny staffing app (StaffPuzzle, 1,200 lines) into htmxr, a server-rendered HTML approach hydrated with htmx and...
og:url: https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe
og:image: https://api.daily.dev/og/posts/SzUdVaHoE.png
og:image:alt: My first attempt at porting a Shiny app failed, and not for a technical reason
og:image:width: 1200
og:image:height: 630
og:locale: en
---

> ## Documentation Index
> Fetch the complete documentation index at: https://daily.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# My first attempt at porting a Shiny app failed, and not for a technical reason

**[R-bloggers](https://daily.dev/sources/rbloggers)** · 18 min read · 0 upvotes · 0 comments

## Summary

A developer recounts rewriting an internal R Shiny staffing app (StaffPuzzle, 1,200 lines) into htmxr, a server-rendered HTML approach hydrated with htmx and backed by plumber2. The first porting attempt failed not for technical reasons but because the author translated Shiny's reactive observers line-by-line instead of rethinking the paradigm. The fix was starting from the data to be served rather than from the reactive graph: inventory screens, cut them into addressable data blocks, give each a URL, and turn controls into URL parameters. Many Shiny constructs (observers for state syncing, updateInput calls, reactiveValues) simply disappear because there is no persistent in-memory state in a route-based hypermedia app. The piece includes concrete before/after code comparisons using Shiny's canonical Old Faithful histogram example, a cost table for common Shiny patterns, and a closing heuristic: if an app's state fits in a URL, porting to htmxr works well; if it requires a complex interdependent reactive graph, Shiny remains the better tool.

## Full article

daily.dev links to this article rather than hosting it. Read it at the original source: <https://www.r-bloggers.com/2026/09/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason>

## Questions this post answers

### How do you migrate a Shiny app to a route-based hypermedia architecture like htmxr without just translating observers to endpoints?

Start from the data being served, not from the reactive graph. Inventory screens by what the user sees, cut each screen into blocks of data, give every block a URL, turn controls into URL parameters, and for each interaction ask whether it mutates persistent business state; if not, it is a GET request rather than an observer-driven update.

_Developers weighing a Shiny-to-htmx rewrite can track migration patterns and postmortems like this on daily.dev._

### What Shiny constructs disappear entirely when porting to htmxr instead of being translated?

Constructs tied to maintaining in-memory reactive state vanish rather than translate: updateSelectInput and the whole update*Input family disappear because htmx just swaps rendered fragments, invalidateLater becomes a trigger attribute like 'every 15s', reactiveValues becomes either a URL parameter or database record, and manual URL-input synchronization code (60 lines in one case) becomes unnecessary since the URL is the state.

_Anyone auditing reactive framework code before a rewrite can compare these tradeoffs via daily.dev._

### Is it worth rewriting a Shiny app as a stateless hypermedia app, or should I stick with Shiny?

The deciding factor is whether the application's state fits entirely in a URL (period, filters, sort order): if so, a hypermedia port to something like htmxr works well, evidenced by a tests-to-code ratio improving from 0.26 to 0.77 with 544 fast tests replacing slow shinytest2 snapshots. If the app has many interdependent reactive values, such as several cross-linked plots recomputing together, Shiny remains the better fit.

_Teams deciding between Shiny and a stateless rewrite can weigh real migration tradeoffs discussed on daily.dev._

## Similar posts on daily.dev

- [Please Just Fucking Try HTMX](https://daily.dev/posts/please-just-fucking-try-htmx-ncu8zbk1v) · Lobsters · 21 upvotes · 3 comments
- [HTMX and Signals: Modern Frontend Without JavaScript Frameworks](https://daily.dev/posts/htmx-and-signals-modern-frontend-without-javascript-frameworks-fotyevidi) · SitePoint · 1 upvotes · 0 comments
- [We chose React to learn it. The AI wrote it for us.](https://daily.dev/posts/we-chose-react-to-learn-it-the-ai-wrote-it-for-us--f4wiwrbv2) · R-bloggers · 2 upvotes · 0 comments
- [After 8 Years of Building With React, I Started My Last Project Without It](https://daily.dev/posts/after-8-years-of-building-with-react-i-started-my-last-project-without-it-niszlr77n) · Medium · 34 upvotes · 9 comments
- [Removing React.js from the codebase and adapting HTMX for UI interactivity](https://daily.dev/posts/removing-react-js-from-the-codebase-and-adapting-htmx-for-ui-interactivity-phhzbmmab) · Hacker News · 2 upvotes · 1 comments

---

Tags: [#r](https://daily.dev/tags/r), [#htmx](https://daily.dev/tags/htmx)

[View this post on daily.dev](https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe)

```json
{"@context":"https://schema.org","@graph":[{"@type":"Organization","@id":"https://daily.dev/#organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180},"sameAs":["https://twitter.com/dailydotdev","https://github.com/dailydotdev","https://www.linkedin.com/company/daily-dev-ltd"]},{"@type":"WebSite","@id":"https://daily.dev/#website","url":"https://daily.dev","name":"daily.dev","publisher":{"@id":"https://daily.dev/#organization"},"potentialAction":{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https://daily.dev/search?q={search_term_string}"},"query-input":"required name=search_term_string"}}]}
{"@context":"https://schema.org","@type":"TechArticle","headline":"My first attempt at porting a Shiny app failed, and not for a technical reason","url":"https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe","mainEntityOfPage":{"@type":"WebPage","@id":"https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe"},"datePublished":"2026-09-29T21:16:08.846Z","dateModified":"2026-09-29T21:16:34.353Z","description":"A developer recounts rewriting an internal R Shiny staffing app (StaffPuzzle, 1,200 lines) into htmxr, a server-rendered HTML approach hydrated with htmx and...","image":"https://media.daily.dev/image/upload/s--qPvKM23u--/f_auto/v1722860399/public/Placeholder%2009","thumbnailUrl":"https://media.daily.dev/image/upload/s--qPvKM23u--/f_auto/v1722860399/public/Placeholder%2009","isAccessibleForFree":true,"articleSection":"R-bloggers","inLanguage":"en","publisher":{"@type":"Organization","name":"daily.dev","url":"https://daily.dev","logo":{"@type":"ImageObject","url":"https://daily.dev/apple-touch-icon.png","width":180,"height":180}},"author":{"@type":"Organization","name":"R-bloggers","logo":"https://media.daily.dev/image/upload/t_logo,f_auto/v1/logos/98bcbbdf72774292a55ceda8f2277f1e","url":"https://daily.dev/sources/rbloggers"},"commentCount":0,"discussionUrl":"https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe","interactionStatistic":[{"@type":"InteractionCounter","interactionType":{"@type":"LikeAction"},"userInteractionCount":0},{"@type":"InteractionCounter","interactionType":{"@type":"CommentAction"},"userInteractionCount":0}],"keywords":"r,htmx","timeRequired":"PT18M"}
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://daily.dev"},{"@type":"ListItem","position":2,"name":"R-bloggers","item":"https://daily.dev/sources/rbloggers"},{"@type":"ListItem","position":3,"name":"My first attempt at porting a Shiny app failed, and not for a technical reason"}]}
{"@context":"https://schema.org","@type":"FAQPage","@id":"https://daily.dev/posts/my-first-attempt-at-porting-a-shiny-app-failed-and-not-for-a-technical-reason-szudvahoe#faq","mainEntity":[{"@type":"Question","name":"How do you migrate a Shiny app to a route-based hypermedia architecture like htmxr without just translating observers to endpoints?","acceptedAnswer":{"@type":"Answer","text":"Start from the data being served, not from the reactive graph. Inventory screens by what the user sees, cut each screen into blocks of data, give every block a URL, turn controls into URL parameters, and for each interaction ask whether it mutates persistent business state; if not, it is a GET request rather than an observer-driven update. Developers weighing a Shiny-to-htmx rewrite can track migration patterns and postmortems like this on daily.dev."}},{"@type":"Question","name":"What Shiny constructs disappear entirely when porting to htmxr instead of being translated?","acceptedAnswer":{"@type":"Answer","text":"Constructs tied to maintaining in-memory reactive state vanish rather than translate: updateSelectInput and the whole update*Input family disappear because htmx just swaps rendered fragments, invalidateLater becomes a trigger attribute like 'every 15s', reactiveValues becomes either a URL parameter or database record, and manual URL-input synchronization code (60 lines in one case) becomes unnecessary since the URL is the state. Anyone auditing reactive framework code before a rewrite can compare these tradeoffs via daily.dev."}},{"@type":"Question","name":"Is it worth rewriting a Shiny app as a stateless hypermedia app, or should I stick with Shiny?","acceptedAnswer":{"@type":"Answer","text":"The deciding factor is whether the application's state fits entirely in a URL (period, filters, sort order): if so, a hypermedia port to something like htmxr works well, evidenced by a tests-to-code ratio improving from 0.26 to 0.77 with 544 fast tests replacing slow shinytest2 snapshots. If the app has many interdependent reactive values, such as several cross-linked plots recomputing together, Shiny remains the better fit. Teams deciding between Shiny and a stateless rewrite can weigh real migration tradeoffs discussed on daily.dev."}}]}
```

