Skip to content

Carolis/hotwire-vs-react

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Rails + Hotwire vs Rails + React

A side-by-side comparison of building the same Todo app with two different approaches.

Quick Start

Terminal 1 - Hotwire app (port 3000):

cd todo-hotwire
bin/rails server

Terminal 2 - React app (port 3001):

cd todo-react
bin/dev

Then open:


Presentation Guide

1. Code Comparison Table

Aspect Hotwire React
Server response HTML fragments JSON data
State location Server (database) Client (useState) + Server
JS lines written ~0 (just markup) ~80 lines
New dependencies None (Rails default) react, react-dom, esbuild
Build step None Required (esbuild)

2. Files to Show Side-by-Side

Controller Comparison

Hotwire (todo-hotwire/app/controllers/todos_controller.rb):

def create
  @todo = Todo.new(todo_params)
  if @todo.save
    respond_to do |format|
      format.turbo_stream  # Returns HTML fragment
      format.html { redirect_to todos_path }
    end
  end
end

React (todo-react/app/controllers/api/todos_controller.rb):

def create
  @todo = Todo.new(todo_params)
  if @todo.save
    render json: @todo, status: :created  # Returns JSON
  end
end

View/Component Comparison

Hotwire (todo-hotwire/app/views/todos/_todo.html.erb):

<%= turbo_frame_tag dom_id(todo) do %>
  <div class="todo-item">
    <%= button_to toggle_todo_path(todo), method: :patch %>
    <span><%= todo.title %></span>
  </div>
<% end %>

React (todo-react/app/javascript/components/TodoItem.jsx):

export default function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div className="todo-item">
      <button onClick={() => onToggle(todo.id)}>
        {todo.completed ? '✓' : '○'}
      </button>
      <span>{todo.title}</span>
    </div>
  );
}

3. DevTools Demo Points

Network Tab

  1. Add a todo in each app
  2. Show the response:
    • Hotwire: HTML fragment like <turbo-stream action="prepend">...</turbo-stream>
    • React: JSON like {"id":1,"title":"Buy milk","completed":false}

Bundle Size

  1. Open DevTools > Network > JS
  2. Compare:
    • Hotwire: ~90KB total (Turbo + Stimulus)
    • React: ~1MB (React + ReactDOM + your code)

React DevTools (React only)

  1. Install React DevTools extension
  2. Show component tree and state updates
  3. Highlight: "This is extra tooling Hotwire doesn't need"

4. Live Demo Script

  1. Start both apps side by side
  2. Create a todo - watch Network tab
  3. Toggle complete - show the request/response difference
  4. Delete - show turbo_stream.remove vs setTodos(filter)
  5. Disable JavaScript (DevTools > Settings > Debugger > Disable JS):
    • Hotwire: Still works! Forms submit normally
    • React: Blank page, completely broken

5. Key Talking Points

Hotwire Pros:

  • No build step needed
  • Smaller bundle size
  • Works without JavaScript (progressive enhancement)
  • Less code to write and maintain
  • No client-side state sync issues

React Pros:

  • Rich ecosystem of components
  • Better for highly interactive UIs
  • Familiar to many frontend developers
  • Great DevTools for debugging
  • More control over rendering

When to use which:

  • Hotwire: CRUD apps, content sites, admin panels, "traditional" web apps
  • React: Real-time collaboration, complex dashboards, SPAs, mobile apps (React Native)

Project Structure

hotwire-vs-react/
├── todo-hotwire/          # Rails 8 + Hotwire (Turbo + Stimulus)
│   ├── app/
│   │   ├── controllers/
│   │   │   └── todos_controller.rb
│   │   └── views/todos/
│   │       ├── index.html.erb
│   │       ├── _todo.html.erb
│   │       └── create.turbo_stream.erb
│   └── ...
│
└── todo-react/            # Rails 8 API + React
    ├── app/
    │   ├── controllers/api/
    │   │   └── todos_controller.rb
    │   └── javascript/components/
    │       ├── App.jsx
    │       ├── TodoForm.jsx
    │       ├── TodoList.jsx
    │       └── TodoItem.jsx
    └── ...

TLDR

Key files

What to Show: Controller Hotwire: todo-hotwire/app/controllers/todos_controller.rb React: todo-react/app/controllers/api/todos_controller.rb ──────────────────────────────────────── What to Show: View/Component Hotwire: todo-hotwire/app/views/todos/_todo.html.erb React: todo-react/app/javascript/components/TodoItem.jsx ──────────────────────────────────────── What to Show: Turbo Stream Hotwire: todo-hotwire/app/views/todos/create.turbo_stream.erb React: (no equivalent) ──────────────────────────────────────── What to Show: State Management Hotwire: Server-side only React: todo-react/app/javascript/components/App.jsx (useState) Demo Highlights

  1. Network Tab: Show HTML fragments (Hotwire) vs JSON (React)
  2. Bundle Size: ~90KB vs ~1MB
  3. Disable JavaScript: Hotwire still works, React breaks
  4. React DevTools: Show component tree (extra tooling needed)

Original Plan

Overview

 Create two separate Rails 8 todo applications to demonstrate the architectural differences between Hotwire
 (Turbo + Stimulus) and React approaches.

 Project Structure

 hotwire-vs-react/
 ├── todo-hotwire/     # Rails 8 + Hotwire (default stack)
 └── todo-react/       # Rails 8 API + React frontend

 ---
 App 1: todo-hotwire (Rails + Hotwire)

 Features

 - Create, toggle complete, delete todos
 - Real-time updates without page refresh (Turbo Frames/Streams)
 - Minimal Stimulus for client-side interactions

 Files to Create

 1. Model: Todo with title:string, completed:boolean
 2. Controller: TodosController with standard CRUD + toggle action
 3. Views:
   - index.html.erb - main list with Turbo Frame
   - _todo.html.erb - partial for single todo (Turbo Stream target)
   - _form.html.erb - form partial
 4. Stimulus: Optional controller for optimistic UI

 Key Code Patterns to Highlight

 - turbo_frame_tag for partial page updates
 - turbo_stream responses for create/update/delete
 - Server-rendered HTML over the wire
 - No JSON serialization needed

 ---
 App 2: todo-react (Rails API + React)

 Features

 - Same functionality: create, toggle, delete
 - React frontend with hooks (useState, useEffect)
 - Fetch API for communication

 Files to Create

 1. Model: Same Todo model
 2. Controller: Api::TodosController returning JSON
 3. React Components:
   - App.jsx - main container with state
   - TodoList.jsx - renders list
   - TodoItem.jsx - single todo with actions
   - TodoForm.jsx - form for new todos
 4. Entry point: Mount React in a Rails view

 Key Code Patterns to Highlight

 - JSON API responses
 - React state management with hooks
 - Client-side rendering
 - Explicit fetch calls and loading states

 ---
 Presentation Guide

 1. Code Comparison Points
 ┌───────────────────┬──────────────────────┬───────────────────────────────┐
 │      Aspect       │       Hotwire        │             React             │
 ├───────────────────┼──────────────────────┼───────────────────────────────┤
 │ Server response   │ HTML fragments       │ JSON data                     │
 ├───────────────────┼──────────────────────┼───────────────────────────────┤
 │ State management  │ Server (database)    │ Client (React state) + Server │
 ├───────────────────┼──────────────────────┼───────────────────────────────┤
 │ JavaScript needed │ ~10 lines (Stimulus) │ ~100+ lines (Components)      │
 ├───────────────────┼──────────────────────┼───────────────────────────────┤
 │ New dependencies  │ None (built-in)      │ react, react-dom, build tools │
 ├───────────────────┼──────────────────────┼───────────────────────────────┤
 │ Controller code   │ Standard Rails       │ API-style with JSON           │
 └───────────────────┴──────────────────────┴───────────────────────────────┘
 2. Files to Show Side-by-Side

 Controllers

 - todos_controller.rb (Hotwire) vs api/todos_controller.rb (React)
 - Show: HTML response vs JSON response

 Views/Components

 - _todo.html.erb vs TodoItem.jsx
 - Show: Server template vs client component

 Creating a Todo

 - Hotwire: Form submits, Turbo Stream appends HTML
 - React: Form calls API, updates state, re-renders

 3. Developer Tools Demo

 Network Tab

 - Hotwire: Show HTML fragments in responses
 - React: Show JSON payloads

 Elements Tab

 - Hotwire: Watch elements update in place
 - React: Show React DevTools component tree

 Performance

 - Hotwire: Smaller JS bundle, faster initial load
 - React: Larger bundle, but faster subsequent interactions

 4. Live Demo Script

 1. Start both apps (port 3000 and 3001)
 2. Add a todo in each - show network requests
 3. Toggle complete - show partial update vs full re-render
 4. Delete - show Turbo Stream remove vs state update
 5. Open DevTools - compare bundle sizes
 6. Disable JavaScript - Hotwire still works (forms), React breaks

 ---
 Implementation Steps

 1. Create todo-hotwire Rails app with scaffold
 2. Add Turbo Frame/Stream enhancements
 3. Create todo-react Rails API app
 4. Add React with esbuild
 5. Build React components
 6. Add minimal styling to both
 7. Test both apps work correctly

 ---
 Verification

 1. Run todo-hotwire on port 3000:
   - Can create, complete, and delete todos
   - No full page reloads
 2. Run todo-react on port 3001:
   - Same functionality
   - React DevTools shows component updates
 3. Compare in browser DevTools:
   - Network responses (HTML vs JSON)
   - Bundle sizes
   - With JS disabled

How to Demonstrate Each Turbo Technique

  1. Turbo Drive (Page Navigation)

What it does: Intercepts link clicks, fetches via AJAX, replaces

Demo steps:

  1. Open DevTools → Network tab

  2. Click "About (Turbo Drive Demo)" link

  3. Show the audience: - Request type is fetch, not document - No browser loading spinner - URL changes, back button works - Content swapped without full reload

  4. Turbo Frames (Partial Updates)

What it does: Only updates the matching element

Demo steps:

  1. Add a few todos
  2. Click "Edit" on one todo
  3. Show the audience: - Only that single row changes to a form - Rest of page is untouched - In Network tab: response contains full page, but only matching frame is used

Code to show:

<%= turbo_frame_tag dom_id(todo) do %> ... <% end %>

<%= turbo_frame_tag dom_id(@todo) do %> <% end %>

  1. Turbo Streams (DOM Manipulation)

What it does: Server sends HTML with actions like append, prepend, replace, remove

Demo steps:

  1. Open Network tab, filter by "Fetch/XHR"
  2. Add a todo → Show response:
  3. Toggle complete → Show response:
  4. Delete a todo → Show response:

Code to show (create.turbo_stream.erb): <%= turbo_stream.prepend "todos", @todo %> <%= turbo_stream.replace "new_todo" do %> <% end %>


Network Tab Responses to Show ┌──────────┬───────────────────────────────────────────────────┐ │ Action │ Turbo Stream Response │ ├──────────┼───────────────────────────────────────────────────┤ │ Create │ │ ├──────────┼───────────────────────────────────────────────────┤ │ Toggle │ │ ├──────────┼───────────────────────────────────────────────────┤ │ Delete │ │ ├──────────┼───────────────────────────────────────────────────┤ │ Navigate │ Full HTML document (Turbo Drive swaps body) │ ├──────────┼───────────────────────────────────────────────────┤ │ Edit │ Full HTML but only used │ └──────────┴───────────────────────────────────────────────────┘

About

Vibecode warning! This is a live demo showcasing the pros and cons of Hotwire vs React from a presentation I did at work

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors