erb javascript ruby 97 lines · 3 tabs

Optimistic toggle button with Stimulus “revert on failure”

Shared by codesnips Jan 2026
3 tabs
<%# locals: item %>
<button
  type="button"
  class="toggle-btn"
  data-controller="toggle"
  data-action="click->toggle#toggle"
  data-toggle-favorited-value="<%= current_user.favorited?(item) %>"
  data-toggle-url-value="<%= favorites_path(item_id: item.id) %>"
  data-toggle-destroy-url-value="<%= favorite_path(item) %>"
  aria-pressed="<%= current_user.favorited?(item) %>">
  <span data-toggle-target="icon" aria-hidden="true">
    <%= current_user.favorited?(item) ? "★" : "☆" %>
  </span>
  <span data-toggle-target="label">
    <%= current_user.favorited?(item) ? "Saved" : "Save" %>
  </span>
</button>

<div class="visually-hidden" aria-live="polite" data-toggle-target="status"></div>
3 files · erb, javascript, ruby Explain with highlit

This snippet shows an optimistic UI toggle — a favourite/bookmark button — built with Rails and Stimulus. The core idea of optimistic UI is to update the interface immediately on click, assuming the server request will succeed, and only roll back if it actually fails. This makes the button feel instant instead of waiting for a network round trip, which matters for high-frequency actions like starring items in a list.

The _toggle.html.erb partial renders the button and wires it to the Stimulus controller through data-controller and data-action. The current server truth is embedded in data-toggle-favorited-value so the controller starts with correct state, and the endpoint plus HTTP method come from data attributes rather than being hardcoded in JavaScript, keeping the controller reusable across any toggle. The CSRF token is pulled from the meta tag so the fetch call passes Rails' forgery protection.

In the toggle_controller.js, the favorited value is a Stimulus typed value, so declaring favoritedValueChanged gives a callback that fires whenever the value flips — this is where render() centralises all DOM updates. On toggle(), the controller flips the value optimistically and disables the button to prevent double-submits, then issues the fetch. The crucial part is the failure path: the original state is captured in previous before the flip, and if the response is not ok or the network throws, favoritedValue is restored, which re-runs render() and visually reverts the button. A small announce() writes to an aria-live region so screen readers hear both success and failure.

The FavoritesController is deliberately idempotent: create finds-or-creates the join record and destroy removes it, each responding with JSON the client ignores on success but checks via response.ok. Because the server is the source of truth, a revert simply means trusting the last-known-good state. The main trade-off is that optimistic UI can briefly show a lie; the mitigation is fast, honest reverts plus disabling the control mid-flight so concurrent clicks cannot desync the state. This pattern suits low-risk, reversible actions and is a poor fit for irreversible or money-moving operations where confirmation should precede the visual change.


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 Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
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

Share this code

Here's the card — post it anywhere.

Optimistic toggle button with Stimulus “revert on failure” — share card
Link copied