javascript erb ruby 93 lines · 3 tabs

Optimistic UI updates with Turbo Streams

Jordan Lee Jan 2026
3 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["count", "button"]
  static values = {
    url: String,
    liked: Boolean
  }

  async toggle(event) {
    event.preventDefault()

    // Optimistic update
    const previousLiked = this.likedValue
    const previousCount = parseInt(this.countTarget.textContent)

    this.likedValue = !previousLiked
    this.countTarget.textContent = previousCount + (this.likedValue ? 1 : -1)
    this.updateButtonState()

    try {
      const response = await fetch(this.urlValue, {
        method: this.likedValue ? 'POST' : 'DELETE',
        headers: {
          'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content,
          'Accept': 'application/json'
        }
      })

      if (!response.ok) {
        throw new Error('Request failed')
      }

      const data = await response.json()
      // Update with actual count from server
      this.countTarget.textContent = data.likes_count

    } catch (error) {
      // Rollback on error
      this.likedValue = previousLiked
      this.countTarget.textContent = previousCount
      this.updateButtonState()

      alert('Failed to update like. Please try again.')
    }
  }

  updateButtonState() {
    if (this.likedValue) {
      this.buttonTarget.classList.add('liked')
      this.buttonTarget.setAttribute('aria-pressed', 'true')
    } else {
      this.buttonTarget.classList.remove('liked')
      this.buttonTarget.setAttribute('aria-pressed', 'false')
    }
  }
}
3 files · javascript, erb, ruby Explain with highlit

Waiting for server confirmation makes interfaces feel sluggish. Optimistic updates immediately show the expected result, then reconcile with the server response. When a user likes a post, I increment the count immediately via Stimulus, submit the request in the background, and handle rollback if it fails. For create operations, I append a pending item with a loading state, then replace it with the real item when the server responds. This pattern requires careful state management—I track pending operations and ensure idempotency so duplicate clicks don't create chaos. The perceived performance improvement is dramatic, but I'm conservative about which operations get optimistic treatment.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs
ruby
class TasksController < ApplicationController
  def destroy
    @task = Task.find(params[:id])
    @task.destroy!

    respond_to do |format|

Remove deleted items instantly with turbo_stream.remove

rails hotwire turbo
by Henry Kim 2 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs

Share this code

Here's the card — post it anywhere.

Optimistic UI updates with Turbo Streams — share card
Link copied