Advice on avoiding duplication between controller actions

I have a Rails application that allows creating new records without leaving the index page. Turbo makes it easy to update the page after creating a record, but I’m wondering how best to avoid duplication between index and create actions. My index page shows a table with the records sorted in a particular order. I could use a Turbo Stream response to append the new record as a row in the table. However, I need to re-render the entire table in order to preserve the sort order. I don’t like putting “index action logic” in the create action simply to render some parts of the index page. This type of thing happens so often in our application that I’m wondering if there is a best practice that I am missing.

Example:

# widgets_controller.rb
def index
  @widgets = Widget.all.order(:name)
end

def create
  @widget = Widget.new(widget_params)
  if @widget.save
    flash.now.notice = "Widget created successfully."
    @widgets = Widget.all.order(:name) # need to add this again because we are replacing the widgets list
  else
    render :new
  end
end

# widgets/create.turbo_stream.erb
<%= turbo_stream.replace :widgets, partial: "widgets", locals: {widgets: @widgets} %>
<%= turbo_stream.replace :flash, partial: "layouts/flash" %>

Aside from moving the “all widgets” logic into a shared private method, is there some sort of best practice for separating index and create/update concerns when using Turbo for this kind of “edit in place” experience?

I don’t see what you mean by “moving the all widgets logic into a shared private method” as I think the problem is implementing logic where it is not desired, rather making it dry. Though you may use the before stream action to add your new widget at the right place : Turbo Reference
This makes you find out after what ID to insert your new record but I agree this is a shame to re-render your whole table just to insert an element. Especially if this table is big.

Or maybe implement the ordering in the view ? I guess the controller is rather pulling the right data rather than formatting it for display, so it would be acceptable.

Your case is actually different from the general use cases, as most apps order through IDs or created_at either asc and desc, (which makes it easy to use append or prepend stream actions) then I guess you can’t avoid the extra work and I am not sure there is a proper way to do it.

What about instead of calculating the @widgets on create action:

  1. perform a redirect_to widgets_path (or whatever is the path for index action)
  2. change widgets/create.turbo_stream.erb to widgets/index.turbo_stream.erb

This way you can keep the logic for calculating/ordering widgets on index action.

I guess you should also change flash.now.notice to flash.notice since it will be now displayed in another action.

1 Like

At one point, I was performing a redirect to the index action in order to keep the index logic scoped to the index action. However, it sort of defeats the purpose of Turbo, since you make the browser perform a second request when it could have been returned in the first request. It would be nice if I could perform a “virtual redirect” in Rails to render the index action without having to tell the browser to make a second request. I don’t think Rails supports that though.

it sort of defeats the purpose of Turbo

I am not sure that this is the purpose of Turbo. You can still ship less js & provide an SPA experience by discarding full page reloads. Additionally in non-Turbo environments it’s pretty common to submit an update request (POST, PATCH, DELETE) and then perform a redirect that shows the result of your action and that’s also the pattern here.

Personally i prefer this way for the sake of maintainability when having to deal with lists that need to be updated totally (because of ordering, pagination etc).

However i get that you could render the index action without having to tell the browser to make a second request. and if you want to do so then moving the “all widgets” logic into a shared private method is the way to do so.