Removing TodosLink to heading


What CRUD example would be complete without the "delete" part? In this section, we focus on deleting todo items using LiveComponent.

Adding a Delete ButtonLink to heading

The first step in deleting todo items is to add a delete button. Typically in Rails this is done using button_to with a delete method.

<div class="TodoItem">
  <% if @editing %>
    <%= form_with(model: @todo_item) do |f| %>
      <%= f.text_field :text %>
      <%= f.submit %>
    <% end %>
  <% else %>
    <%= @todo_item.text %>
    <%= button_tag(
      "Edit",
      data: { action: "click->todoitemcomponent#edit" }
    ) %>
    <%= button_to(
      "Delete",
      todo_list_todo_item_path(@todo_item.todo_list_id, @todo_item),
      rerender: :parent,
      method: :delete,
      form: {
        data: {
          turbo: true,
          turbo_stream: true
        }
      }
    ) %>
  <% end %>
</div>
<div class="TodoItem">
  <% if @editing %>
    <%= form_with(model: @todo_item) do |f| %>
      <%= f.text_field :text %>
      <%= f.submit %>
    <% end %>
  <% else %>
    <%= @todo_item.text %>
    <%= button_tag(
      "Edit",
      data: { action: "click->todoitemcomponent#edit" }
    ) %>
    <%= button_to(
      "Delete",
      todo_list_todo_item_path(@todo_item.todo_list_id, @todo_item),
      rerender: :parent,
      method: :delete,
      form: {
        data: {
          turbo: true,
          turbo_stream: true
        }
      }
    ) %>
  <% end %>
</div>

The destroy actionLink to heading

When the delete button gets clicked, Rails will call the destroy action, which deletes the record from the database (note that the other actions we wrote earlier have been omitted for brevity).

class TodoItemsController < ApplicationController
  def destroy
    TodoItem.delete(params[:id])
  end
end
class TodoItemsController < ApplicationController
  def destroy
    TodoItem.delete(params[:id])
  end
end

Finally, we need to rerender the todo list excluding the deleted item. Since items are managed via ViewComponent slots, we can iterate through them and remove the item with the matching ID:

<%= live.rerender do |todo_list_component| %>
  <% todo_list_component.todo_items.reject! do |todo_item_component| %>
    <% todo_item_component.todo_item.id == params[:id].to_i %>
  <% end %>
<% end %>
<%= live.rerender do |todo_list_component| %>
  <% todo_list_component.todo_items.reject! do |todo_item_component| %>
    <% todo_item_component.todo_item.id == params[:id].to_i %>
  <% end %>
<% end %>

Preventing N + 1 QueriesLink to heading

Behind the scenes, LiveComponent serializes the todo list and todo item ActiveRecord objects so they can be sent to the front-end. By default and to save space, none of the record's attributes are included. When the component's state is sent back to the server for rerender, any ActiveRecord objects are converted into instances of LiveComponent::RecordProxy, a wrapper class that automatically fetches the corresponding record from the database whenever any attribute method is called.

In the case above where we're re-rendering the entire todo list (as opposed to a single todo item), this behavior can result in making a database query per item, which can be quite inefficient.

To prevent LiveComponent from unnecessarily fetching records, consider including the fields your component needs to render in the list of serialized attributes:

class TodoItemComponent < ApplicationComponent
  include LiveComponent::Base

  serializes :todo_item, with: :model_serializer, attributes: [:text, :todo_list_id]

  attr_reader :todo_item, :editing

  def initialize(todo_item:, editing: false)
    @todo_item = todo_item
    @editing = editing
  end
end
class TodoItemComponent < ApplicationComponent
  include LiveComponent::Base

  serializes :todo_item, with: :model_serializer, attributes: [:text, :todo_list_id]

  attr_reader :todo_item, :editing

  def initialize(todo_item:, editing: false)
    @todo_item = todo_item
    @editing = editing
  end
end

By including the text and todo_list_id attributes in the list of serialized attributes, calling eg. todo_item.text will use the existing attribute value and avoid loading the item from the database.

Other TechniquesLink to heading

LiveComponent fully supports ActiveRecord objects, but in many cases it can be less error-prone to only pass the data to a component that it actually needs to render. In other words, it's probably a good idea to avoid passing ActiveRecord objects into your components, and instead pass individual attributes or an attributes hash.

For example, here's how we might render our todo list in app/views/todo_lists/show.html.erb:

<%= render(TodoListComponent.new(todo_list: @todo_list.attributes)) do |todo_list_component| %>
  <% @todo_list.todo_items.each do |todo_item| %>
    <% todo_list_component.with_todo_item(todo_item: todo_item.attributes) %>
  <% end %>
<% end %>
<%= render(TodoListComponent.new(todo_list: @todo_list.attributes)) do |todo_list_component| %>
  <% @todo_list.todo_items.each do |todo_item| %>
    <% todo_list_component.with_todo_item(todo_item: todo_item.attributes) %>
  <% end %>
<% end %>