State TrackingLink to heading

LiveComponent automatically tracks the state of your components. In fact, the key innovation behind LiveComponent is recognizing that a component can be entirely reconstituted from its state independent of the context or surrounding markup.

Keyword argumentsLink to heading

In a framework like React, state is manually tracked via calls to hooks like useState. State can also be derived from props, the arguments a component receives from its parent. When props or state changes, React automatically re-renders the component.LiveComponent follows a similar pattern. A component's arguments - or more specifically, it's keyword arguments - are tracked automatically. These arguments can be modified client-side and sent to the server for re-render. This is done via this.render in JavaScript, and closely mirrors the Ruby API. Here's an example we saw when building our todo list application:
import { live, LiveController } from "@camertron/live-component";

type TodoItemComponentProps = {
  editing: boolean;
}

@live("TodoItemComponent") // this is the Ruby class name
export class TodoItemComponent extends LiveController<TodoItemComponentProps> {
  edit() {
    this.render((component) => {
      component.props.editing = true;
    });
  }
}
import { live, LiveController } from "@camertron/live-component";

type TodoItemComponentProps = {
  editing: boolean;
}

@live("TodoItemComponent") // this is the Ruby class name
export class TodoItemComponent extends LiveController<TodoItemComponentProps> {
  edit() {
    this.render((component) => {
      component.props.editing = true;
    });
  }
}
Behind the scenes, LiveComponent assembles a JSON data structure with the updated editing argument. LiveComponent's server-side rendering mechanism then parses the JSON and sends the keyword arguments into a new instance of the TodoItemComponent. The state object looks something like this (parts have been omitted for brevity):
{
  "ruby_class": "TodoItemComponent",
  "props": {
    "__lc_attributes": {
      "data-id": "d7fd8295-1f62-4b2f-99cb-178d86749e10",
      "_lc_symkeys": []
    },
    "editing": true
  },
  "slots": {},
  "children": {},
  "content": null
}
{
  "ruby_class": "TodoItemComponent",
  "props": {
    "__lc_attributes": {
      "data-id": "d7fd8295-1f62-4b2f-99cb-178d86749e10",
      "_lc_symkeys": []
    },
    "editing": true
  },
  "slots": {},
  "children": {},
  "content": null
}

Thinking in terms of stateLink to heading

Any state the component should "remember" between re-renders must be passed as a keyword argument to the component's initializer. When the component's state is serialized to be sent to the front-end, LiveComponent reads instance variables with the same name as each keyword argument. There is no secondary mechanism for telling LiveComponent about additional bits of state to track. This is an intentional design decision. Requiring all state to be represented in the initializer's method signature provides several benefits:
  1. Explicitness: All the state a component tracks can be seen in one place.
  2. Simplicity: State is specified as regular 'ol arguments to a regular 'ol Ruby class. There's no fancy state objects at play or a specialized API for getting and setting state.
  3. Predictability: A component's state always consists of the same key/value pairs, even if some state is optional some of the time.

Derived stateLink to heading

All "rememberable" state must be represented in the component's keyword arguments, which includes derived state (state computed from other state). While it might be tempting to track derived state using only instance variables, remember that there is no other mechanism for tracking state aside from the initializer. In practice this usually means adding a keyword argument with a default value of nil.For example, let's say we wanted to record how much time it took the user to edit a todo item. The component starts out in non-edit mode, which means there is no starting time. The timer starts when the user switches to edit mode, and stops when they're done. Here's how you might implement such a feature:
class TodoItemComponent < ApplicationComponent
  include LiveComponent::Base

  def initialize(todo_item:, editing: false, start_time: nil, elapsed_time: nil)
    @todo_item = todo_item
    @editing = editing
    @start_time = start_time

    if @editing && @start_time.nil?
      @start_time = Time.now
    elsif !@editing && @start_time.present?
      @elapsed_time = Time.now - @start_time
      @start_time = nil
    end
  end

  private

  def label_text
    if @start_time
      "Editing..."
    elsif @elapsed_time
      "User spent #{@elapsed_time} seconds editing"
    else
      "Never edited"
    end
  end

  def button_text
    @start_time ? "Stop editing" : "Edit"
  end
end
class TodoItemComponent < ApplicationComponent
  include LiveComponent::Base

  def initialize(todo_item:, editing: false, start_time: nil, elapsed_time: nil)
    @todo_item = todo_item
    @editing = editing
    @start_time = start_time

    if @editing && @start_time.nil?
      @start_time = Time.now
    elsif !@editing && @start_time.present?
      @elapsed_time = Time.now - @start_time
      @start_time = nil
    end
  end

  private

  def label_text
    if @start_time
      "Editing..."
    elsif @elapsed_time
      "User spent #{@elapsed_time} seconds editing"
    else
      "Never edited"
    end
  end

  def button_text
    @start_time ? "Stop editing" : "Edit"
  end
end
Here's the corresponding template:
<div><%= label_text %></div>
<button data-action="click->timercomponent#on_click">
  <%= button_text %>
</button>
<div><%= label_text %></div>
<button data-action="click->timercomponent#on_click">
  <%= button_text %>
</button>
And here's the client-side code:
import { live, LiveController } from "@camertron/live-component";

type TimerProps = {
  editing: boolean
}

@live("TimerComponent")
export class TimerComponent extends LiveController<TimerProps> {
  on_click() {
    this.render((component) => {
      component.props.editing = !component.props.editing;
    });
  }
}
import { live, LiveController } from "@camertron/live-component";

type TimerProps = {
  editing: boolean
}

@live("TimerComponent")
export class TimerComponent extends LiveController<TimerProps> {
  on_click() {
    this.render((component) => {
      component.props.editing = !component.props.editing;
    });
  }
}
Note how the component tracks derived state by adding arguments to the initializer and making use of default values.