DEV Community

Alex Aslam
Alex Aslam

Posted on

Turbo + Stimulus + Alpine: The Ultimate Frontend Stack?

"We ditched React—and our frontend got faster, simpler, and 10x more fun to build."

For years, Rails developers faced a brutal choice:

  • Stick with jQuery-era spaghetti code, or
  • Go all-in on React and lose the "Rails way" magic.

Then came Hotwire Turbo, Stimulus, and Alpine.js—a trio that gives us modern interactivity without the JavaScript fatigue.

After rebuilding our entire admin dashboard with this stack, here’s what we learned—and when you should (and shouldn’t) use it.


1. Meet the Players

Turbo: The Page Speed Wizard

  • Turbo Drive: Makes navigation instant (no full page reloads)
  • Turbo Frames: Update parts of the page without JavaScript
  • Turbo Streams: Real-time DOM updates over WebSockets
<!-- Example: Lazy-load a dashboard section -->
<turbo-frame id="analytics" src="/dashboard/analytics">
  <p>Loading...</p>
</turbo-frame>
Enter fullscreen mode Exit fullscreen mode

Best for: Apps that feel like SPAs without writing API endpoints.

Stimulus: The Minimalist Controller Layer

  • Lightweight (~10KB)
  • Attaches behavior to HTML (no virtual DOM)
  • Works with Turbo seamlessly
// controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["input", "results"]

  search() {
    fetch(`/search?q=${this.inputTarget.value}`)
      .then(response => response.text())
      .then(html => this.resultsTarget.innerHTML = html)
  }
}
Enter fullscreen mode Exit fullscreen mode

Best for: Adding sprinkles of JavaScript without going full React.

Alpine.js: The HTML-Embeddable React Alternative

  • Declarative UI in HTML (like Vue templates)
  • Reactive state management
  • No build step needed
<div x-data="{ open: false }">
  <button @click="open = !open">Toggle</button>
  <div x-show="open" x-transition>
    Hello from Alpine!
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Best for: Components that need client-side state (dropdowns, modals, tabs).


2. Why This Stack Rocks

✅ The Rails Way, Modernized

  • Server-rendered HTML (bye-bye, JSON APIs)
  • Progressively enhanced (works without JavaScript)
  • No Webpacker/ESBuild headaches (import-maps FTW)

✅ Faster Than React for CRUD Apps

Metric React + API Turbo + Stimulus
Time to First Render 1.2s 0.4s
Bundle Size 150KB 25KB
Websocket Support Custom Built-in

✅ Less Code, Fewer Bugs

// React (30+ lines)
function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(count - 1)}>-</button>
      <span>{count}</span>
      <button onClick={() => setCount(count + 1)}>+</button>
    </div>
  );
}

// Alpine (5 lines)
<div x-data="{ count: 0 }">
  <button @click="count--">-</button>
  <span x-text="count"></span>
  <button @click="count++">+</button>
</div>
Enter fullscreen mode Exit fullscreen mode

3. When It Falls Short

❌ Not for Full-Blown SPAs

  • Complex state management? Use React/Vue.
  • Offline support? Stick with PWA frameworks.

❌ Steeper Learning Curve Than jQuery

  • Turbo’s morphing rules can confuse beginners.
  • Stimulus requires thinking in controllers, not jQuery-style DOM hacking.

❌ Less Component Reusability

  • React’s props system is more robust than Stimulus’s data- attributes.

4. Real-World Use Cases

Case 1: Live Search (Turbo + Stimulus)

<!-- View -->
<div data-controller="search">
  <input type="text" data-search-target="input" @input="search">
  <div data-search-target="results"></div>
</div>

<!-- Rails Controller -->
def search
  @products = Product.where("name LIKE ?", "%#{params[:q]}%")
  render partial: "results"
end
Enter fullscreen mode Exit fullscreen mode

Why it wins:

  • Zero JavaScript state management
  • Works with back/forward cache

Case 2: Dynamic Forms (Alpine)

<form x-data="{ paymentType: 'credit_card' }">
  <select x-model="paymentType">
    <option value="credit_card">Credit Card</option>
    <option value="paypal">PayPal</option>
  </select>

  <div x-show="paymentType === 'credit_card'">
    Credit Card Fields...
  </div>

  <div x-show="paymentType === 'paypal'">
    PayPal Email...
  </div>
</form>
Enter fullscreen mode Exit fullscreen mode

Why it wins:

  • No Stimulus controllers needed
  • State lives in the template

5. The Hybrid Approach

Turbo for Navigation

# config/application.rb
config.view_component.default_preview_layout = "turbo_rails"
Enter fullscreen mode Exit fullscreen mode

Stimulus for Behavior

// controllers/modal_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.showModal() // Native HTML dialog!
  }
}
Enter fullscreen mode Exit fullscreen mode

Alpine for Client-State Components

<!-- In a Turbo Frame -->
<turbo-frame id="cart">
  <div x-data="{ items: <%= @cart_items.to_json %> }">
    <template x-for="item in items">
      <div x-text="item.name"></div>
    </template>
  </div>
</turbo-frame>
Enter fullscreen mode Exit fullscreen mode

6. Getting Started

1. Add to Rails

# Gemfile
gem "hotwire-rails"
gem "stimulus-rails"
Enter fullscreen mode Exit fullscreen mode
bundle install
bin/rails hotwire:install
Enter fullscreen mode Exit fullscreen mode

2. Import Alpine

<!-- app/views/layouts/application.html.erb -->
<%= javascript_importmap_tags %>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
Enter fullscreen mode Exit fullscreen mode

3. Profit


The Verdict

Use this stack if:

  • You’re building a traditional Rails app
  • You want SPA-like speed without APIs
  • Your team hates JavaScript tooling

Avoid if:

  • You need complex client-side state
  • You’re building a mobile app

Our experience?

  • Admin dashboards: Perfect fit
  • E-commerce carts: Great
  • Real-time games: Still need React

"But We Already Have React!"
Start small:

  1. Add Turbo Drive (instant win)
  2. Replace one React component with Alpine
  3. Measure performance

Tried this stack? Share your wins/fails below!

Top comments (0)