Why I Ditched Vue.js and Went Back to Rails Views

A contrarian take on modern web development: sometimes simple beats sophisticated


The Setup

In 2017, PropertyWebBuilder was cutting-edge:

  • EmberJS admin panel
  • Vue.js frontend components
  • Rails API backend
  • Deployed to Heroku with Webpack

The stack felt modern, professional, and aligned with industry trends. SPAs were the future. Server-rendered HTML was "legacy."

Seven years later, in 2024, I ripped it all out and went back to Rails views with Turbo.

This is why.


What We Had (The SPA Approach)

The Good Parts

Vue.js gave us:
- Reactive property search (instant filtering without page reloads)
- Smooth image galleries with transitions
- Interactive map integration
- Component reusability

Example Vue component:

<template>
  <div class="property-card">
    <img :src="property.featured_image" :alt="property.title">
    <h3>{{ property.title }}</h3>
    <p>{{ formatPrice(property.price) }}</p>
    <button @click="toggleFavorite">
      <i :class="favoriteIcon"></i>
    </button>
  </div>
</template>

<script>
export default {
  props: ['property'],
  data() {
    return {
      isFavorite: false
    }
  },
  computed: {
    favoriteIcon() {
      return this.isFavorite ? 'fas fa-heart' : 'far fa-heart'
    }
  },
  methods: {
    formatPrice(price) {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: 'USD'
      }).format(price)
    },
    toggleFavorite() {
      this.isFavorite = !this.isFavorite
      // API call to save favorite
    }
  }
}
</script>

This felt sophisticated. We had state management, reactivity, and a clear separation of concerns.

The Pain Points

But underneath the polish, problems festered:

1. Build Complexity

Our package.json had 47 dependencies. Webpacker configuration was 300+ lines. Every deployment:

# Rails asset compilation
RAILS_ENV=production rails assets:precompile

# Plus Webpack
NODE_ENV=production webpack --config webpack.config.js

# Build time: ~4 minutes
# Failed builds: ~20% of the time (random dependency issues)

2. SEO Nightmare

Property listings are content. Google needs to index them. But our Vue SPA:

  • Rendered blank HTML initially
  • Required JavaScript to display properties
  • Needed complex server-side rendering setup (which we never implemented)

Google Search Console:
- Pages indexed: 23 (out of 200+ properties)
- Core Web Vitals: "Needs improvement"

3. Two Codebases to Maintain

Every feature meant:
1. Rails API endpoint
2. Vue component
3. State management
4. API integration
5. Error handling (twice — frontend + backend)

Example: Adding a new property field

# 1. Rails migration
add_column :pwb_properties, :year_built, :integer

# 2. Rails serializer
def year_built
  object.year_built
end

# 3. Vue API call
async fetchProperty(id) {
  const response = await axios.get(`/api/properties/${id}`)
  this.property = response.data
}

# 4. Vue template
<p>Built in {{ property.year_built }}</p>

# 5. Vue form
<input v-model="property.year_built" type="number">

Total files touched: 6
Lines of code: ~50
Potential bug locations: 6

4. Dependency Hell

Our yarn.lock was 14,000 lines. We had:
- Vue 2.x (Vue 3 migration would be massive)
- Webpack 4 (Webpack 5 breaking changes)
- 45 other packages with their own dependencies

Security vulnerabilities: A constant stream.

npm audit
found 23 vulnerabilities (8 moderate, 15 high)

Every npm install was a gamble.

5. Performance Overhead

Our JavaScript bundle was 487 KB (minified).

For a property listing site, users waited for:
1. HTML to load
2. JavaScript to download (487 KB)
3. JavaScript to parse and execute
4. API call to fetch properties
5. Vue to render

Time to interactive: 3.2 seconds (on 3G)

For what? Displaying 12 property cards.


The Breaking Point

In 2024, upgrading to Rails 8 meant dealing with:
- Webpacker → Importmaps migration
- Vue 2 → Vue 3 (breaking changes)
- Webpack 4 → 5 (more breaking changes)
- EmberJS admin panel (totally outdated)

I looked at the codebase and asked:

"What does this JavaScript complexity actually give us?"

The honest answer: Not enough.


The Rewrite (Rails Views + Turbo)

Decision: Embrace Server-Side Rendering

Rails 7+ includes Hotwire (Turbo + Stimulus). It promised:
- Server-rendered HTML
- SPA-like interactions
- No build step
- Simple deployment

I was skeptical. Could it really replace Vue?

Migration Strategy

Step 1: Convert Vue Components to Rails Partials

Before (Vue):

<property-card :property="property"></property-card>

After (Rails partial):

<%= render 'properties/card', property: property %>

Step 2: Replace Client-Side Routing with Turbo Frames

Before (Vue Router):

{
  path: '/properties/:id',
  component: PropertyShow
}

After (Turbo Frames):

<%= turbo_frame_tag "property_#{property.id}" do %>
  <%= render 'properties/show', property: property %>
<% end %>

Step 3: Interactive Elements with Stimulus

For things that needed JavaScript (like favoriting), we used Stimulus (Rails' lightweight JS framework):

// app/javascript/controllers/favorite_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["icon"]

  toggle() {
    const propertyId = this.element.dataset.propertyId

    fetch(`/favorites/${propertyId}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    }).then(response => {
      if (response.ok) {
        this.iconTarget.classList.toggle('fas')
        this.iconTarget.classList.toggle('far')
      }
    })
  }
}

HTML:

<div data-controller="favorite" data-property-id="<%= property.id %>">
  <button data-action="click->favorite#toggle">
    <i data-favorite-target="icon" class="far fa-heart"></i>
  </button>
</div>

Lines of code: 15 (vs. 50+ for Vue version)
Dependencies: 0 (Stimulus ships with Rails)


The Results

Build Complexity: Eliminated

Before:

"dependencies": {
  "vue": "^2.6.14",
  "vue-router": "^3.5.3",
  "vuex": "^3.6.2",
  "axios": "^0.27.2",
  // ... 43 more packages
}

After:

{}

No package.json. No webpack.config.js. No build step.

Deployment time: 4 minutes → 45 seconds

SEO: Fixed

Google Search Console (3 months after rewrite):
- Pages indexed: 187 (out of 200+)
- Core Web Vitals: "Good"
- Click-through rate: +34%

Why? Google sees fully-rendered HTML immediately. No JavaScript required.

Performance: Massively Improved

Bundle size:
- Before: 487 KB JavaScript
- After: 37 KB (Turbo + Stimulus)

Time to interactive:
- Before: 3.2s (3G)
- After: 0.8s (3G)

Lighthouse score:
- Before: 68/100
- After: 94/100

Development Speed: Faster

Adding a new property field:

# 1. Migration
add_column :pwb_properties, :year_built, :integer

# 2. View
<p>Built in <%= property.year_built %></p>

# 3. Form
<%= form.number_field :year_built %>

Total files: 3
Lines of code: 15
Time: 5 minutes

(Previously: 6 files, 50 lines, 20 minutes)

Maintenance: Trivial

Security vulnerabilities:
- Before: Weekly npm audit warnings
- After: Handled by bundle update

Dependency updates:
- Before: Fear-based (will Webpack break?)
- After: Routine Rails upgrades


What We Lost

Honesty time: We did lose some things.

1. Instant Search Filtering

Before: Type in search → Vue filters results instantly (client-side)

After: Type in search → Submit → Server filters → Page refresh (with Turbo, no full reload)

Impact: Slightly less "magical" feeling. But still fast (<200ms).

Trade-off worth it? Yes. Most users submit the form anyway.

2. Fancy Transitions

Before: Vue transitions between states (fade in/out, slide)

After: Basic CSS transitions

Impact: Minimal. Users didn't notice.

3. Component Reusability (Kind Of)

Vue's component system is elegant:

<PropertyCard :property="property" :show-favorite="true" />

Rails partials are... less elegant:

<%= render 'properties/card', property: property, show_favorite: true %>

But: ViewComponents (Rails gem) bridges this gap:

<%= render PropertyCardComponent.new(property: property, show_favorite: true) %>

Impact: Solved with a 50-line Ruby class.


What We Gained

1. Simplicity

One language (Ruby), one framework (Rails), one deployment target.

Team onboarding:
- Before: "Learn Rails, Vue, Webpack, state management..."
- After: "Learn Rails."

2. Reliability

Deployment success rate:
- Before: ~80% (Webpack failures, dependency conflicts)
- After: ~99%

3. Speed (Development)

Features ship faster. No context-switching between Rails and Vue.

Example: Adding multi-language support

  • Before: i18n in Rails + i18n in Vue (two systems)
  • After: Rails i18n (one system)

Time saved: ~40%

4. Accessibility

Server-rendered HTML is inherently accessible. Vue SPAs require extra work:

  • Screen readers
  • Keyboard navigation
  • Progressive enhancement

With Rails views: Most accessibility is automatic.


When Vue (or React) Makes Sense

I'm not saying SPAs are always wrong. They're excellent for:

Highly Interactive Apps

  • Figma (complex canvas manipulation)
  • Notion (real-time collaboration)
  • Google Sheets (spreadsheet interactions)

Real-Time Features

  • Chat applications
  • Collaborative editing
  • Live dashboards

Offline-First Apps

  • PWAs that work without internet
  • Mobile apps (React Native)

But for content-heavy sites? (Blogs, e-commerce, real estate, etc.)

Server-side rendering + Turbo/htmx is often better.


The Controversial Take

The JavaScript community made us believe:

"You need React/Vue for a modern web app."

This is not true.

You need JavaScript for:
- Highly interactive UIs
- Real-time collaboration
- Offline functionality

You don't need it for:
- Displaying content
- Simple forms
- CRUD operations
- Most websites

Rails (and Django, Laravel, Phoenix) got sidelined in favor of JAMstack and SPAs.

The pendulum is swinging back.

Tools like:
- Hotwire (Rails)
- htmx (any backend)
- LiveView (Phoenix/Elixir)
- Inertia.js (Laravel)

...prove you can have SPA-like UX without SPA complexity.


Lessons Learned

1. Choose Boring Technology

Vue.js was exciting in 2017. But "exciting" ages poorly.

Rails views are boring. They'll work the same way in 2030.

2. Optimize for Change

The codebase that's easiest to modify wins long-term.

Vue required changing 6 files per feature.
Rails views: 2-3 files.

Over time, this compounds.

3. Question Hype

Just because everyone uses React doesn't mean you should.

Ask:
- Do I actually need client-side rendering?
- Is this complexity worth it?
- Can I achieve 90% of the UX with 10% of the code?

4. Leverage Your Framework

Rails 8 includes:
- Turbo (SPA-like navigation)
- Stimulus (lightweight JS)
- Importmaps (no bundler)
- ActionCable (WebSockets)
- ActiveStorage (file uploads)

Why add Webpack, Vue, and 47 npm packages?


Conclusion

PropertyWebBuilder is faster, simpler, and easier to maintain after ditching Vue.js.

The metrics:
- 📉 JavaScript: 487 KB → 37 KB
- ⚡ Time to interactive: 3.2s → 0.8s
- 📈 SEO: 23 pages indexed → 187 pages
- 🚀 Deploy time: 4 min → 45 sec
- 🛠️ Dependencies: 47 npm packages → 0

The lesson:

Sometimes the "legacy" approach is actually the modern one.

Server-rendered HTML isn't outdated. It's been patiently waiting for us to remember why it worked in the first place.


Further Reading


Disagree? That's fine. If Vue/React works for you, great. But if you're drowning in JavaScript complexity for a content-heavy site, consider going back to basics.

It might just be the most modern choice you make.


Follow the PropertyWebBuilder journey on GitHub or find me on Twitter [@etewiah]

PropertyWebBuilder — Open-source Rails engine for real estate websites

GitHub · Articles · Twitter