LiveCable is a Phoenix LiveView-style live component system for Ruby on Rails that tracks state server-side and allows you to call actions from the frontend using Stimulus with a React style state management API.
- Server-side state management: Component state is maintained on the server using ActionCable
- Reactive variables: Automatic UI updates when state changes with smart change tracking
- Automatic change detection: Arrays, Hashes, and ActiveRecord models automatically trigger updates when mutated
- Action dispatch: Call server-side methods from the frontend
- Lifecycle hooks: Hook into component lifecycle events
- Stimulus integration: Seamless integration with Stimulus controllers and blessings API
Add this line to your application's Gemfile:
gem 'live_cable'And then execute:
bundle installTo use LiveCable, you need to set up your ApplicationCable::Connection to initialize a LiveCable::Connection.
Add this to your app/channels/application_cable/connection.rb:
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :live_connection
def connect
self.live_connection = LiveCable::Connection.new(self.request)
end
end
endRegister the LiveController in your Stimulus application (app/javascript/controllers/application.js):
import { Application } from "@hotwired/stimulus"
import LiveController from "live_cable_controller"
import "live_cable" // Automatically starts DOM observer
const application = Application.start()
application.register("live", LiveController)The live_cable import automatically starts a DOM observer that watches for LiveCable components and transforms custom attributes (live-action, live-form, live-reactive, etc.) into Stimulus attributes.
If you want to call LiveCable actions from your own Stimulus controllers, add the LiveCable blessing:
import { Application, Controller } from "@hotwired/stimulus"
import LiveController from "live_cable_controller"
import LiveCableBlessing from "live_cable_blessing"
import "live_cable" // Automatically starts DOM observer
// Enable the blessing for all controllers
Controller.blessings = [
...Controller.blessings,
LiveCableBlessing
]
const application = Application.start()
application.register("live", LiveController)This adds the liveCableAction(action, params) method to all your Stimulus controllers:
// In your custom controller
export default class extends Controller {
submit() {
// Dispatch an action to the LiveCable component
this.liveCableAction('save', {
title: this.titleTarget.value
})
}
}The action will be dispatched as a DOM event that bubbles up to the nearest LiveCable component. This is useful when you need to trigger LiveCable actions from custom controllers or third-party integrations.
LiveCable's subscription manager keeps connections alive between renders if the Stimulus controller is disconnected and reconnected, for example, sorting a list of live components. The subscription is only destroyed when the parent component does another render cycle and sees that the child is no longer rendered.
- Reduced WebSocket churn: No reconnection overhead during navigation
- State preservation: Server-side state persists across page transitions
- Better performance: Eliminates subscription setup/teardown cycles
- No race conditions: Avoids issues from rapid connect/disconnect
Subscription persistence is handled automatically. Components are identified by their live_id, and the
subscription manager ensures each component has exactly one active subscription at any time.
When the server sends a destroy status, the subscription is removed from the client side and the server side
channel is destroyed and unsubscribed.
Note on component location and namespacing:
- Live components must be defined inside the
Live::module so they can be safely loaded from a string name. - We recommend placing component classes under
app/live/(soLive::Countermaps toapp/live/counter.rb). - Corresponding views should live under
app/views/live/...(e.g.app/views/live/counter/component.html.live.erb). - When rendering a component from a view, pass the namespaced underscored path, e.g.
live/counter(which camelizes toLive::Counter).
LiveCable uses ActiveModel::Callbacks to provide lifecycle callbacks that you can hook into at different stages of a component's lifecycle.
-
before_connect/after_connect: Called when the component is first subscribed to the channel. Useafter_connectfor initializing timers, subscribing to external services, or loading additional data. -
before_disconnect/after_disconnect: Called when the component is unsubscribed from the channel. Usebefore_disconnectfor cleanup: stop timers, unsubscribe from external services, or save state before disconnection. -
before_render/after_render: Called before and after each render and broadcast, including the initial render. Usebefore_renderfor preparing data, performing calculations, or validating state. Useafter_renderfor triggering side effects or cleanup after the DOM has been updated.
Use standard ActiveModel callback syntax to register your callbacks:
module Live
class ChatRoom < LiveCable::Component
reactive :messages, -> { [] }
reactive :timer_id, -> { nil }
after_connect :start_polling_timer
before_disconnect :stop_timer
after_render :log_render_time
actions :add_message
def add_message(params)
messages << { text: params[:text], timestamp: Time.current }
end
private
def start_polling_timer
# Start a timer after connection is established
self.timer_id = SetInterval.new(5.seconds) do
check_for_updates
end
end
def stop_timer
# Clean up timer before disconnecting
timer_id&.cancel
end
def log_render_time
Rails.logger.info "Rendered at #{Time.current}"
end
end
endYou can also use callbacks with conditionals:
module Live
class Dashboard < LiveCable::Component
before_connect :authenticate_user, if: :requires_auth?
after_render :track_analytics, unless: :development_mode?
private
def requires_auth?
# Your auth logic
end
def development_mode?
Rails.env.development?
end
end
endWhen a component is subscribed:
- Component is instantiated
before_connectcallbacks are called- Connection is established and stream starts
after_connectcallbacks are calledbefore_rendercallbacks are called- Component is rendered and broadcast
after_rendercallbacks are called
On subsequent updates (action calls, reactive variable changes):
- State changes occur
before_rendercallbacks are called- Component is rendered and broadcast
after_rendercallbacks are called
When a component is unsubscribed:
before_disconnectcallbacks are called- Connection is cleaned up and streams are stopped
after_disconnectcallbacks are called- Component is cleaned up
# app/components/live/counter.rb
module Live
class Counter < LiveCable::Component
reactive :count, -> { 0 }
actions :increment, :decrement
def increment
self.count += 1
end
def decrement
self.count -= 1
end
end
endComponent templates should start with a root element. LiveCable will automatically add the necessary attributes to wire up the component:
<%# app/views/live/counter/component.html.live.erb %>
<div>
<h2>Counter: <%= count %></h2>
<button live-action="increment">+</button>
<button live-action="decrement">-</button>
</div>LiveCable automatically injects the required attributes (live-id, live-component, live-actions, and live-defaults) into your root element and transforms them into Stimulus attributes.
For better performance, you should use .html.live.erb templates instead of .html.erb. These templates use the Herb templating engine to parse your template and send only the updated parts over the wire, rather than the full HTML on every render:
<%# app/views/live/counter/component.html.live.erb %>
<div>
<h2>Counter: <%= count %></h2>
<button live-action="increment">+</button>
<button live-action="decrement">-</button>
</div>Important notes about .live.erb files:
- They should only be used for component templates, not for regular Rails views
- They return a special partial object that tracks static and dynamic parts, not a string
- They're optional—
.html.erbfiles, and other templating systems will work, but they will send full template diffs on changes. - When a
.live.erbtemplate is first rendered, LiveCable sends the full template and tracks which parts are static - On subsequent renders, only the changed dynamic parts are sent to the client
- This can significantly reduce bandwidth and improve performance for frequently updating components
LiveCable uses Herb (an ERB parser) and Prism (a Ruby parser) to analyze your .live.erb templates at compile time and determine which parts need to be re-rendered when reactive variables change.
Template Parsing and Dependency Analysis:
When a .live.erb template is compiled, LiveCable:
-
Splits the template into parts: Herb parses the template and identifies static text, Ruby code blocks (
<% ... %>), and expressions (<%= ... %>) -
Analyzes each dynamic part with Prism: For every Ruby code block and expression, Prism parses the Ruby code into an Abstract Syntax Tree (AST)
-
Tracks dependencies: A
DependencyVisitorwalks the AST to identify:- Reactive variable reads: Direct references to reactive variables (e.g.,
count,step) - Component method calls: Methods called on the component (e.g.,
component.products,total_price) - Local variable dependencies: Local variables defined in one part and used in another
- Reactive variable reads: Direct references to reactive variables (e.g.,
-
Builds metadata: Each dynamic part gets metadata about what it depends on:
{ component_dependencies: [:count, :step], # Reactive vars this part reads component_method_calls: [:products], # Methods called on component local_dependencies: [:user], # Locals from previous parts defines_locals: [:total] # Locals this part defines }
Method Dependency Expansion:
For component methods, LiveCable goes deeper by analyzing the method implementation itself:
# Component class
def total_price
items.sum { |item| item.price * item.quantity }
endThe MethodAnalyzer uses Prism to parse the component's source file and build a dependency graph:
total_pricemethod reads theitemsreactive variable- When
itemschanges, any template part callingcomponent.total_priceis re-rendered
This analysis is done once and cached, creating a transitive dependency map for all component methods.
Runtime Re-rendering Decision:
When a reactive variable changes (e.g., self.count = 5), LiveCable:
-
Receives change notification: The change tracking system reports which variables changed (e.g.,
[:count]) -
Evaluates each template part: For each dynamic part, the
PartialRendererchecks:should_skip_part?(changes, component_dependencies, component_method_calls, local_dependencies)
-
Expands method dependencies: If the part calls
component.products, the method analyzer expands this to find which reactive variablesproductsdepends on -
Makes the skip decision:
- Skip if none of the part's dependencies changed
- Render if any component dependency, method dependency, or local dependency changed
- Always render on initial render (
:all) or template switches (:dynamic)
-
Returns selective updates: Only the parts that need updating are rendered and sent to the client as an array:
[nil, nil, "<span>5</span>", nil, "<button>Reset</button>"] # ^ ^ └─ changed ^ └─ changed # | └─ skipped └─ skipped # └─ skipped
Example: Counter Template Analysis
Given this template:
<div>
<h2>Counter: <%= count %></h2>
<button live-action="increment">+ <%= step %></button>
<button live-action="reset">Reset</button>
<input name="step" value="<%= step %>" live-reactive>
</div>LiveCable analyzes and tracks:
<h2>Counter: <%= count %></h2>depends oncount<button>+ <%= step %></button>depends onstep<button>Reset</button>is static (never changes)<input value="<%= step %>">depends onstep
When count changes: Only the <h2> element is re-rendered and sent to the client.
When step changes: Both the button label and input value are re-rendered and sent.
The Reset button is never re-rendered since it contains no dynamic content.
Local Variable Tracking:
Templates can define local variables that are used in later parts:
<% user = component.current_user %>
<% total = component.calculate_total %>
<div>Welcome <%= user.name %></div>
<div>Total: <%= total %></div>LiveCable tracks:
- First part defines
userandtotal(always executes to define locals) - Second part depends on
userlocal (re-renders only ifuserlocal was redefined) - Third part depends on
totallocal (re-renders only iftotallocal was redefined)
The mark_locals_dirty mechanism ensures that if a local is recomputed (because its dependencies changed), any parts using that local are also re-rendered.
Performance Benefits:
This intelligent analysis provides significant performance improvements:
- Reduced bandwidth: Only changed HTML fragments are sent over WebSocket
- Faster rendering: Server only executes code for parts that changed
- No client-side diffing needed: Client receives exact parts to update
- Efficient method calls: Component methods are only called if their dependencies changed
- Cache-friendly: Dependency analysis happens once at compile time, not on every render
Render components using the live helper method:
<%# Simple usage %>
<%= live('counter', id: 'my-counter') %>
<%# With default values %>
<%= live('counter', id: 'my-counter', count: 10, step: 5) %>The live helper automatically:
- Creates component instances with unique IDs
- Wraps the component in proper Stimulus controller attributes
- Passes default values to reactive variables
- Reuses existing component instances when navigating back
If you already have a component instance, use render directly:
<%
@counter = Live::Counter.new('my-counter')
@counter.count = 10
%>
<%= render(@counter) %>Reactive variables automatically trigger re-renders when changed. Define them with default values using lambdas:
module Live
class ShoppingCart < LiveCable::Component
reactive :items, -> { [] }
reactive :discount_code, -> { nil }
reactive :total, -> { 0.0 }
actions :add_item, :remove_item, :apply_discount
def add_item(params)
items << { id: params[:id], name: params[:name], price: params[:price].to_f }
calculate_total
end
def remove_item(params)
items.reject! { |item| item[:id] == params[:id] }
calculate_total
end
def apply_discount(params)
self.discount_code = params[:code]
calculate_total
end
private
def calculate_total
subtotal = items.sum { |item| item[:price] }
discount = discount_code ? apply_discount_rate(subtotal) : 0
self.total = subtotal - discount
end
def apply_discount_rate(subtotal)
discount_code == "SAVE10" ? subtotal * 0.1 : 0
end
end
endLiveCable automatically tracks changes to reactive variables containing Arrays, Hashes, and ActiveRecord models. You can mutate these objects directly without manual re-assignment:
module Live
class TaskManager < LiveCable::Component
reactive :tasks, -> { [] }
reactive :settings, -> { {} }
reactive :project, -> { Project.find_by(id: params[:project_id]) }
actions :add_task, :update_setting, :update_project_name
# Arrays - direct mutation triggers re-render
def add_task(params)
tasks << { title: params[:title], completed: false }
end
# Hashes - direct mutation triggers re-render
def update_setting(params)
settings[params[:key]] = params[:value]
end
# ActiveRecord - direct mutation triggers re-render
def update_project_name(params)
project.name = params[:name]
end
end
endChange tracking works recursively through nested structures:
module Live
class Organization < LiveCable::Component
reactive :data, -> { { teams: [{ name: 'Engineering', members: [] }] } }
actions :add_member
def add_member(params)
# Deeply nested mutation - automatically triggers re-render
data[:teams].first[:members] << params[:name]
end
end
endWhen you store an Array, Hash, or ActiveRecord model in a reactive variable:
- Automatic Wrapping: LiveCable wraps the value in a transparent Delegator
- Observer Attachment: An Observer is attached to track mutations
- Change Detection: When you call mutating methods (
<<,[]=,update, etc.), the Observer is notified - Smart Re-rendering: Only components with changed variables are re-rendered
This means you can write natural Ruby code without worrying about triggering updates:
# These all work and trigger updates automatically:
tags << 'ruby'
tags.concat(%w[rails rspec])
settings[:theme] = 'dark'
user.update(name: 'Jane')Primitive values (String, Integer, Float, Boolean, Symbol) cannot be mutated in place, so you must reassign them:
reactive :count, -> { 0 }
reactive :name, -> { "" }
# ✅ This works (reassignment)
self.count = count + 1
self.name = "John"
# ❌ This won't trigger updates (mutation, but primitives are immutable)
self.count.+(1)
self.name.concat("Doe")For more details on the change tracking architecture, see ARCHITECTURE.md.
Shared variables allow multiple components on the same connection to access the same state. There are two types:
Shared reactive variables trigger re-renders on all components that use them:
module Live
class ChatMessage < LiveCable::Component
reactive :messages, -> { [] }, shared: true
reactive :username, -> { "Guest" }
actions :send_message
def send_message(params)
messages << { user: username, text: params[:text], time: Time.current }
end
end
endWhen any component updates messages, all components using this shared reactive variable will re-render.
Use shared (without reactive) when you need to share state but don't want updates to trigger re-renders in the component that doesn't display that data:
module Live
class FilterPanel < LiveCable::Component
shared :cart_items, -> { [] } # Access cart but don't re-render on cart changes
reactive :filter, -> { "all" }
actions :update_filter
def update_filter(params)
self.filter = params[:filter]
# Can read cart_items.length but changing cart elsewhere won't re-render this
end
end
end
module Live
class CartDisplay < LiveCable::Component
reactive :cart_items, -> { [] }, shared: true # Re-renders on cart changes
actions :add_to_cart
def add_to_cart(params)
cart_items << params[:item]
# CartDisplay re-renders, but FilterPanel does not
end
end
endUse case: FilterPanel can read the cart to show item count in a badge, but doesn't need to re-render every time an item is added—only when the filter changes.
For security, explicitly declare which actions can be called from the frontend:
module Live
class Secure < LiveCable::Component
actions :safe_action, :another_safe_action
def safe_action
# This can be called from the frontend
end
def another_safe_action(params)
# This can also be called with parameters
end
private
def internal_method
# This cannot be called from the frontend
end
end
endNote on params argument: The params argument is optional. Action methods only receive params if you declare the argument in the method signature:
# These are both valid:
def increment
self.count += 1 # No params needed
end
def add_todo(params)
todos << params[:text] # Params are used
endIf you don't need parameters from the frontend, simply omit the params argument from your method definition.
The params argument is an ActionController::Parameters instance, which means you can use strong parameters and all the standard Rails parameter handling methods:
module Live
class UserProfile < LiveCable::Component
reactive :user, ->(component) { User.find(component.defaults[:user_id]) }
reactive :errors, -> { {} }
actions :update_profile
def update_profile(params)
# Use params.expect (Rails 8+) or params.require/permit for strong parameters
user_params = params.expect(user: [:name, :email, :bio])
if user.update(user_params)
self.errors = {}
else
self.errors = user.errors.messages
end
end
end
endYou can also use assign_attributes if you want to validate before saving:
def update_profile(params)
user_params = params.expect(user: [:name, :email, :bio])
user.assign_attributes(user_params)
if user.valid?
user.save
self.errors = {}
else
self.errors = user.errors.messages
end
endThis works seamlessly with form helpers:
<form live-form="update_profile">
<div>
<label>Name</label>
<input type="text" name="user[name]" value="<%= user.name %>" />
<% if errors[:name] %>
<span class="error"><%= errors[:name].join(", ") %></span>
<% end %>
</div>
<div>
<label>Email</label>
<input type="email" name="user[email]" value="<%= user.email %>" />
<% if errors[:email] %>
<span class="error"><%= errors[:email].join(", ") %></span>
<% end %>
</div>
<div>
<label>Bio</label>
<textarea name="user[bio]"><%= user.bio %></textarea>
</div>
<button type="submit">Update Profile</button>
</form>LiveCable provides custom HTML attributes that are automatically transformed into Stimulus attributes. These attributes use a shortened syntax similar to Stimulus but are more concise.
Triggers a component action when an event occurs.
Syntax:
live-action="action_name"- Uses Stimulus default event (click for buttons, submit for forms)live-action="event->action_name"- Custom eventlive-action="event1->action1 event2->action2"- Multiple actions
Examples:
<!-- Default event (click) -->
<button live-action="save">Save</button>
<!-- Custom event -->
<button live-action="mouseover->highlight">Hover Me</button>
<!-- Multiple actions -->
<button live-action="click->save focus->track_focus">Save and Track</button>Transformation: live-action="save" becomes data-action="live#action_$save"
Serializes a form and submits it to a component action.
Syntax:
live-form="action_name"- Uses Stimulus default event (submit)live-form="event->action_name"- Custom eventlive-form="event1->action1 event2->action2"- Multiple actions
Examples:
<!-- Default event (submit) -->
<form live-form="save">
<input type="text" name="title">
<button type="submit">Save</button>
</form>
<!-- On change event -->
<form live-form="change->filter">
<select name="category">...</select>
</form>
<!-- Multiple actions -->
<form live-form="submit->save change->auto_save">
<input type="text" name="content">
</form>Transformation: live-form="save" becomes data-action="live#form_$save"
Passes parameters to actions on the same element.
Syntax: live-value-param-name="value"
Examples:
<!-- Single parameter -->
<button live-action="update" live-value-id="123">Update Item</button>
<!-- Multiple parameters -->
<button live-action="create"
live-value-type="task"
live-value-priority="high">
Create Task
</button>Transformation: live-value-id="123" becomes data-live-id-param="123"
Updates a reactive variable when an input changes.
Syntax:
live-reactive- Uses Stimulus default event (input for text fields)live-reactive="event"- Single eventlive-reactive="event1 event2"- Multiple events
Examples:
<!-- Default event (input) -->
<input type="text" name="username" value="<%= username %>" live-reactive>
<!-- Specific event -->
<input type="text" name="search" live-reactive="keydown">
<!-- Multiple events -->
<input type="text" name="query" live-reactive="keydown keyup">Transformation: live-reactive becomes data-action="live#reactive", and live-reactive="keydown" becomes data-action="keydown->live#reactive"
Adds debouncing to reactive and form updates to reduce network traffic.
Syntax: live-debounce="milliseconds"
Examples:
<!-- Debounced reactive input (300ms delay) -->
<input type="text" name="search" live-reactive live-debounce="300">
<!-- Debounced form submission (1000ms delay) -->
<form live-form="change->filter" live-debounce="1000">
<select name="category">...</select>
</form>Transformation: live-debounce="300" becomes data-live-debounce-param="300"
<div>
<h2>Search Products</h2>
<!-- Reactive search with debouncing -->
<input type="text"
name="query"
value="<%= query %>"
live-reactive
live-debounce="300">
<!-- Form with multiple actions and parameters -->
<form live-form="submit->filter change->auto_filter" live-debounce="500">
<select name="category">
<option value="all">All</option>
<option value="electronics">Electronics</option>
</select>
</form>
<!-- Action buttons with parameters -->
<button live-action="add_to_cart"
live-value-product-id="<%= product.id %>"
live-value-quantity="1">
Add to Cart
</button>
<!-- Multiple events -->
<button live-action="click->save mouseover->preview">
Save & Preview
</button>
</div>When a form action is triggered, the controller manages potential race conditions with pending reactive updates:
- Priority: Any pending debounced
reactivemessage is sent immediately before the form action message in the same payload. - Order: This guarantees that the server applies the reactive update first, then the form action.
- Debounce Cancellation: Any pending debounced form or reactive submissions are canceled, ensuring only the latest state is processed.
This mechanism prevents scenarios where a delayed reactive update (e.g., from typing quickly) could arrive after a form submission and overwrite the changes made by the form action.
LiveCable supports special HTML attributes to control how the DOM is updated during morphing.
When live-ignore is present on an element, LiveCable (via morphdom) will skip updating that element's children during a re-render.
- Usage:
<div live-ignore>...</div> - Behavior: Prevents the element's content from being modified by server updates.
- Default: Live components automatically have this attribute to ensure the parent component doesn't overwrite the child component's state.
The live-key attribute acts as a hint for the diffing algorithm to identify elements in a list. This allows elements to be reordered rather than destroyed and recreated, preserving their internal state (like input focus or selection).
- Usage:
<div live-key="unique_id">...</div> - Behavior: Matches elements across renders to maintain identity.
- Notes:
- The key must be unique within the context of the parent element.
idattributes are also used as keys iflive-keyis not present, butlive-keyis preferred in loops to avoid ID collisions or valid HTML ID constraints.- Do not use array indices as keys; use a stable identifier from your data (e.g., database ID). If you reorder or add / remove elements from your array the index will no longer match the proper component.
Example:
<% todos.each do |todo| %>
<li live-key="<%= todo.id %>">
...
</li>
<% end %>By default, components render the partial at app/views/live/component_name.html.live.erb. You can organize your templates differently by marking a component as compound.
module Live
class Checkout < LiveCable::Component
compound
# Component will look for templates in app/views/live/checkout/
end
endWhen compound is used, the component will look for its template in a directory named after the component. By default, it renders app/views/live/component_name/component.html.live.erb.
Override the template_state method to dynamically switch between different templates:
module Live
class Wizard < LiveCable::Component
compound
reactive :current_step, -> { "account" }
reactive :form_data, -> { {} }
actions :next_step, :previous_step
def template_state
current_step # Renders app/views/live/wizard/account.html.live.erb, etc.
end
def next_step(params)
form_data.merge!(params)
self.current_step = case current_step
when "account" then "billing"
when "billing" then "confirmation"
else "complete"
end
end
def previous_step
self.current_step = case current_step
when "billing" then "account"
when "confirmation" then "billing"
else current_step
end
end
end
endThis creates a multi-step wizard with templates in:
app/views/live/wizard/account.html.live.erbapp/views/live/wizard/billing.html.live.erbapp/views/live/wizard/confirmation.html.live.erbapp/views/live/wizard/complete.html.live.erb
You can call component methods instead of storing large datasets in reactive variables.
Why this matters: Reactive variables are stored in memory in the server-side container. For large datasets (like paginated results), this can add up quickly and consume unnecessary memory.
Best practice: Use reactive variables for state (like page numbers, filters), but call methods to fetch data on-demand during rendering:
module Live
class ProductList < LiveCable::Component
reactive :page, -> { 0 }
reactive :category, -> { "all" }
actions :next_page, :prev_page, :change_category
def products
# Fetched fresh on each render, not stored in memory
Product.where(category_filter)
.offset(page * 20)
.limit(20)
end
def next_page
self.page += 1
end
def prev_page
self.page = [page - 1, 0].max
end
def change_category(params)
self.category = params[:category]
self.page = 0
end
private
def category_filter
category == "all" ? {} : { category: category }
end
end
end.live.erb templates automatically forward method calls to your component through method_missing, so you can call component methods and reactive variables directly:
<%# app/views/live/product_list/component.html.live.erb %>
<div class="products">
<% products.each do |product| %>
<div class="product">
<h3><%= product.name %></h3>
<p><%= product.price %></p>
</div>
<% end %>
</div>
<div class="pagination">
<button live-action="prev_page">Previous</button>
<span>Page <%= page + 1 %></span>
<button live-action="next_page">Next</button>
</div>If you're using regular .erb files or other templating languages, you must use the component local to access component methods and reactive variables:
<%# app/views/live/product_list/component.html.erb %>
<div class="products">
<% component.products.each do |product| %>
<div class="product">
<h3><%= product.name %></h3>
<p><%= product.price %></p>
</div>
<% end %>
</div>
<div class="pagination">
<button live-action="prev_page">Previous</button>
<span>Page <%= component.page + 1 %></span>
<button live-action="next_page">Next</button>
</div>This approach:
- Keeps only
pageandcategoryin memory (lightweight) - Fetches the 20 products fresh on each render
- Prevents memory bloat when dealing with large datasets
- Still provides reactive updates when
pageorcategorychanges
LiveCable components can subscribe to ActionCable channels using the stream_from method. This allows components to react to real-time broadcasts from anywhere in your application, making it easy to build collaborative features like chat rooms, live notifications, or shared dashboards.
Call stream_from in the after_connect lifecycle callback to subscribe to a channel:
module Live
module Chat
class ChatRoom < LiveCable::Component
reactive :messages, -> { [] }, shared: true
after_connect :subscribe_to_chat
private
def subscribe_to_chat
stream_from("chat_messages", coder: ActiveSupport::JSON) do |data|
messages << data
end
end
end
end
endAny part of your application can broadcast to the stream using ActionCable's broadcast API:
module Live
module Chat
class ChatInput < LiveCable::Component
reactive :message
actions :send_message
def send_message(params)
return if params[:message].blank?
message_data = {
id: SecureRandom.uuid,
text: params[:message],
timestamp: Time.now.to_i,
user: current_user.as_json(only: [:id, :first_name, :last_name])
}
# Broadcast to the chat stream
ActionCable.server.broadcast("chat_messages", message_data)
# Clear the input
self.message = ""
end
end
end
endWhen a broadcast is received:
- The stream callback is executed with the broadcast payload
- You can update reactive variables inside the callback
- LiveCable automatically detects the changes and broadcasts updates to all affected components
- All components sharing the same reactive variables are re-rendered
- Automatic re-rendering: Changes to reactive variables inside stream callbacks trigger re-renders
- Shared state: Combine with
shared: truereactive variables to sync state across multiple component instances - Connection-scoped: Each user's component instances receive broadcasts independently
- Coder support: Use
coder: ActiveSupport::JSONto automatically decode JSON payloads
- Chat applications: Real-time message updates across all participants
- Live notifications: Push notifications to specific users or groups
- Collaborative editing: Sync changes across multiple users viewing the same document
- Live dashboards: Update metrics and charts in real-time
- Presence tracking: Show who's currently online or viewing a resource
This project is available as open source under the terms of the MIT License.