Reactive Variables
Reactive variables are the heart of LiveCable's state management. When a reactive variable changes, the component automatically re-renders and broadcasts the updated HTML to connected clients.
Defining Reactive Variables
Define reactive variables using the reactive class method with a lambda that provides the default value:
module Live
class ShoppingCart < LiveCable::Component
reactive :items, -> { [] }
reactive :discount_code, -> { nil }
reactive :total, -> { 0.0 }
actions :add_item, :apply_discount
end
endWhy Lambdas?
Default values are defined as lambdas to ensure each component instance gets its own copy of the value. Without lambdas, all instances would share the same object reference.
Setting Reactive Variables
Use the setter method (with self.) to update reactive variables:
def add_item(params)
items << { id: params[:id], name: params[:name], price: params[:price].to_f }
self.total = calculate_total(items)
end
def apply_discount(params)
self.discount_code = params[:code]
self.total = calculate_total_with_discount(items, discount_code)
endAutomatic Change Tracking
LiveCable 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
endHow It Works
When 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')Nested Structures
Change 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
endPrimitive Values
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 (primitives are immutable)
self.count.+(1)
self.name.concat("Doe")Shared Variables
Shared variables allow multiple components on the same connection to access the same state.
Shared Reactive Variables
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.
Shared Non-Reactive Variables
Use shared (without reactive) when you need to share state but don't want updates to trigger re-renders:
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.
Accessing Reactive Variables in Views
Reactive variables are automatically available as local variables in your component views:
<%= live_component(component) do %>
<div class="shopping-cart">
<h2>Shopping Cart</h2>
<p>Items: <%= items.size %></p>
<p>Total: $<%= total %></p>
<% if discount_code %>
<p class="discount">Discount code: <%= discount_code %></p>
<% end %>
<ul>
<% items.each do |item| %>
<li><%= item[:name] %> - $<%= item[:price] %></li>
<% end %>
</ul>
</div>
<% end %>Default Values from Rendering
You can pass default values when rendering a component:
<%# Set initial count to 10 %>
<%= live 'counter', id: 'my-counter', count: 10 %>
<%# Load user data %>
<%= live 'profile', id: "profile-#{@user.id}", user_id: @user.id %>These defaults are only applied when the component is first created, not on subsequent renders.