This page is part of a series that describes building a complete todo list application with LiveComponent. Please consider reading it from the beginning.
What CRUD example would be complete without the "delete" part? In this section, we focus on deleting todo items using LiveComponent.
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>
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 %>
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.
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 %>