Maybe I am missing something here but it looks like if you are using Turbo and Action Cable in Rails 7, you could with a race condition by broadcasting to yourself.
For example, let's say that I have a todo list and when you add an item, there is a create.turbo_stream.erb
that does a turbo_stream.append 'todoList', partial: ...
.
So when you add an item it gets appended to #todoList
, fine.
BUT let's say you also have an action cable or Turbo::StreamsChannel broadcast that happens after_create
, like this:
Turbo::StreamsChannel.broadcast_append_to('todo-stream', {
target: 'todoList',
partial: 'todo',
locals: { todo: todo }
})
So now what happens is that the create.turbo_stream.html
appends the item and then broadcast ALSO appends the element to the todo list!
I was under the assumption that the broadcast would be "smart" enough to know that the client who triggered the broadcast should not receive the broadcast, but that doesnt seem to be the case.
So how can I handle this? I cant just replace the create.turbo_stream.html file with the broadcast as more complex methods, such as dropzone.js or sortable.js, result in having the final element in a certain place, so broadcasting a replacement doesnt make sense.
First, make sure you have a unique id for your todo partial, this will prevent appending duplicated elements and replace them instead:
# app/views/todos/_todo.html.erb
<%= tag.div id: dom_id(todo) do %>
# ...
<% end %>
If the template’s first element has an id that is already used by a direct child inside the container targeted by dom_id, it is replaced instead of appended.
https://turbo.hotwired.dev/reference/streams#append
If replacing a duplicated id
is somehow causing an issue, the only solution I've found is to replicate what refresh
action does by tracking current request id:
class Todo < ApplicationRecord
# this will broadcast a turbo stream with request id attribute:
# <turbo-stream request-id="b3937918-30cb-47de-8bb0-020ac0d5c2dd" action="append" target="todos">
# ...
# </turbo-stream>
after_create_commit do
broadcast_append_to(:todos, attributes: {"request-id": Turbo.current_request_id})
end
end
# app/controllers/todos_controller.rb
def create
@todo = Todo.new(todo_params)
if @todo.save
render turbo_stream: turbo_stream.append(:todos, @todo)
end
end
The request id is sent from the front end as X-Turbo-Request-Id
header.
When this turbo stream arrives you need to compare request-id
attribute with a recent request. If there is a match that means turbo stream came from a broadcast that you've initiated and can be ignored:
// app/javascript/application.js
const originalAppend = Turbo.StreamActions.append
// override append action and do what the `refresh` action does:
Turbo.StreamActions.append = function() {
const session = Turbo.session
const isRecentRequest = this.requestId && session.recentRequests.has(this.requestId)
if (!isRecentRequest && !session.navigator.currentVisit) {
originalAppend.bind(this).call()
}
}
https://github.com/hotwired/turbo/blob/v8.0.5/src/core/streams/stream_actions.js#L52
https://github.com/hotwired/turbo/blob/v8.0.5/src/core/session.js#L110