Hopefully these emails help you generally, but today this one is a reminder for me.
Jonathan, destroy_all
affects the results of the original query. You can’t just blindly reuse it:
class PostsController < ApplicationController
def delete_a_lot
@posts = Post.where(id: params[:post_ids])
@posts.destroy_all # nope
render turbo_stream: @posts.map { turbo_stream.remove post }
end
end
The @posts
array is always blank when it hits the render call—even though the posts are deleted. The correction is to update the “nope” line above to:
@posts = @posts.destroy_all
Here’s the gotcha: it’s not just about what destroy_all
returns. It’s about how ActiveRecord queries work.
When you assign @posts = Post.where(...)
, you’re creating an ActiveRecord::Relation, a query object. Calling destroy_all
on that relation pulls the records, destroys them, and returns the deleted records. But—and here’s the kicker—the original @posts
relation still exists. So when you use it again in the render call, Rails re-executes the query… and gets nothing, because the posts are already gone.
Effectively, your code becomes:
@query1 = Post.where(id: params[:post_ids])
@query1.destroy_all
@query2 = Post.where(id: params[:post_ids]) # always []
render turbo_stream: @query2.map { |post| turbo_stream.remove post }
That’s why you must reassign @posts
to the result of destroy_all
. You’re not just saving what was returned—you’re sidestepping the original query from being lazily re-evaluated after the data is gone.
@posts = @posts.destroy_all
Alternatively, just assign it directly:
@posts = Post.where(id: params[:post_id).destroy_all
Now @posts
contains the deleted records in memory, and you can use them to render the appropriate Turbo Stream responses without surprises.