Architecture
Understanding LiveCable's architecture helps you build better components and debug issues effectively.
High-Level Overview
Browser Server
┌─────────────────┐ ┌──────────────────────┐
│ Stimulus │ │ LiveCable │
│ Controller │◄─────►│ Component │
│ │ Cable │ │
│ - UI Events │ │ - State │
│ - DOM Updates │ │ - Business Logic │
└─────────────────┘ │ - Rendering │
└──────────────────────┘Component Lifecycle
1. Initial Render (Server-Side)
When a page loads:
User Request → Rails Controller → View renders `live` helper
→ Component instantiated
→ Component rendered to HTML
→ HTML sent to browserAt this stage, the component has no live connection yet. It's just plain HTML with Stimulus data attributes.
2. WebSocket Connection
When the page loads in the browser:
Stimulus Controller connects → ActionCable subscription
→ Server creates/retrieves component
→ Component.mark_connected called (once)
→ before_connect callbacks run
→ Connection established
→ after_connect callbacks run
→ Component broadcasts initial state3. User Interaction
When the user interacts with the component:
User clicks button → Stimulus dispatches action
→ ActionCable sends message to server
→ Server calls whitelisted action method
→ Action updates reactive variables
→ Container marks variables as dirty
→ before_render callbacks run
→ Component re-renders
→ after_render callbacks run
→ HTML broadcasted to client
→ morphdom updates DOM4. Reconnection
When the user navigates away and back:
User navigates away → Stimulus controller disconnects
→ Subscription persists (not destroyed)
→ Component instance kept alive
User navigates back → Stimulus controller reconnects
→ Reuses existing subscription
→ Component already exists
→ mark_connected returns early (already connected)
→ Component broadcasts current stateCore Components
LiveCable::Component
The base class for all live components.
Responsibilities:
- Define reactive variables and shared state
- Whitelist callable actions
- Implement business logic
- Render views
Key Methods:
reactive- Define reactive variablesactions- Whitelist action methodsrender_broadcast- Trigger a render and broadcast
LiveCable::Connection
Manages the lifecycle of components for a single WebSocket connection.
Responsibilities:
- Store component instances
- Manage containers (state storage)
- Route actions to components
- Coordinate rendering
Key Methods:
add_component- Register a new componentget/set- Read/write reactive variablesdirty- Mark variables as changedbroadcast_changeset- Render all dirty components
LiveCable::Container
Stores reactive variable values for a component.
Responsibilities:
- Store variable values
- Track which variables are dirty
- Wrap values in Delegators for change tracking
- Attach observers to track mutations
Key Features:
- Hash subclass for simple key-value storage
- Automatic wrapping of Arrays, Hashes, and ActiveRecord models
- Changeset tracking for efficient re-rendering
LiveCable::Delegator
Transparent proxy for Arrays, Hashes, and ActiveRecord models.
Responsibilities:
- Intercept mutating method calls
- Notify observers when changes occur
- Support nested structures
Example:
# When you do this:
items << 'new item'
# Behind the scenes:
delegator = Delegator::Array.new(['item1'])
delegator.add_live_cable_observer(observer, :items)
delegator << 'new item' # Calls observer.notify(:items)LiveCable::Observer
Notifies containers when delegated values change.
Responsibilities:
- Receive change notifications from Delegators
- Mark variables as dirty in containers
Change Tracking System
How Mutations Trigger Re-renders
Reactive variable is set:
rubyself.items = []Container wraps value in Delegator:
rubycontainer[:items] = Delegator.create_if_supported([], :items, observer)User mutates the value:
rubyitems << 'new item'Delegator notifies observer:
rubydef <<(value) result = super notify_observers result endObserver marks variable dirty:
rubydef notify(variable) container.mark_dirty(variable) endContainer adds to changeset:
rubydef mark_dirty(*variables) @changeset |= variables endAfter action completes, broadcast changeset:
rubydef broadcast_changeset components.each do |component| if container_changed? || shared_variables_changed? component.render_broadcast end end end
Subscription Persistence
Traditional ActionCable subscriptions are destroyed when Stimulus controllers disconnect. LiveCable keeps them alive:
Without Persistence (Standard ActionCable)
Page Load → Subscribe → Connected
Navigate Away → Disconnect → Subscription destroyed → WebSocket closed
Navigate Back → Subscribe → New connection → New WebSocketWith Persistence (LiveCable)
Page Load → Subscribe → Connected
Navigate Away → Controller disconnects → Subscription persists
Navigate Back → Controller reconnects → Reuses subscription → Same componentBenefits:
- Reduced WebSocket overhead
- State preservation across navigation
- No race conditions from rapid connect/disconnect
- Better performance
Implementation: The subscription manager (in the JavaScript controller) tracks subscriptions by live_id and only creates new subscriptions when needed.
Rendering Pipeline
morphdom Integration
LiveCable uses morphdom to efficiently update the DOM:
- Server sends new HTML
- morphdom diffs against current DOM
- Only changed elements are updated
- Event listeners and component state preserved
Special Attributes
live-ignore: Skip updating this element and its childrenlive-key: Identity hint for list items (preserves DOM elements during reordering)
Example:
<% items.each do |item| %>
<li live-key="<%= item.id %>">
<%= item.name %>
</li>
<% end %>When items are reordered, morphdom uses live-key to move existing elements instead of destroying and recreating them.
Security
Action Whitelisting
Only explicitly declared actions can be called from the frontend:
actions :safe_method, :another_safe_method
def safe_method
# Callable from frontend
end
def internal_method
# Not callable - will raise error
endCSRF Protection
LiveCable includes CSRF token validation on all WebSocket messages:
- Token is embedded in the Stimulus controller
- Token is sent with every action
- Server validates token before processing
- Invalid tokens are rejected
Performance Considerations
Efficient Re-rendering
- Only components with dirty variables are re-rendered
- Changesets are reset after each broadcast cycle
- morphdom minimizes actual DOM manipulations
Memory Management
- Components are cleaned up when connections close
- Containers are destroyed when components are removed
- Observers are detached when values are replaced
Scalability
- Each WebSocket connection has its own component instances
- Shared variables use a single container per connection
- ActionCable handles WebSocket scaling natively
Debugging Tips
Enable Logging
# config/environments/development.rb
config.action_cable.log_level = :debugInspect Component State
In browser console:
// Get all LiveCable controllers
Stimulus.controllers.filter(c => c.identifier === 'live')
// Get component data
controller.element.dataset.liveIdCheck Changeset
Add logging to your components:
after_render do
Rails.logger.debug "Rendered #{self.class.name}"
Rails.logger.debug "Changesets: #{live_connection.containers.inspect}"
end