Component with collection not re-rendering when one element changes
davidalejandroaguilar opened this issue · 2 comments
Bug report
Given a component with a collection of objects, when you update one of them, the component is not rendered again.
Description
For example, given a post list component that renders posts:
class PostListComponent < ViewComponent::Base
include Motion::Component
attr_reader :posts
def initialize(posts:)
@posts = posts
end
map_motion :post_list_update
def post_list_update(event)
post = posts.find_by(id: event.target.data[:post_id])
post.randomize_date_within!(5.days)
# Only by changing posts in some way do we get a re-render, even though a
# post was changed.
#
posts.each(&:reload)
end
end
<div>
<% posts.each do |post| %>
<button data-motion="post_list_update" data-post-id="<%= post.id %>">PostListComponent#update</button>
<%= render(PostComponent.new(post: post)) %>
<% end %>
</div>
When you update one of those posts, the component will not reflect the update.
I'm just trying out View Component and Motion, so please let me know if this is the expected behavior and what the actual behavior should be. I'd be happy to document this.
Reproduce the issue
I have created a demo repo here, it has a sqlite db so I think it should be ready to go. Else, just create a post. Then:
- Click the "PostListComponent#update" button. You can see how the component re-renders.
- Uncomment the
posts.each(&:reload)
line inPostListComponent
and see how the component won't re-render even though a post was changed.
Thank you for putting together this detailed report!
I took a look at the repo, and I do think that Motion is working correctly in this case.
The reason that the component is not updating until you reload
is that find_by
always fetches a fresh copy of the record from the database. Since this new instance never finds its way into the state of the component (either as an instance variable itself or an entry in @posts
), from the component's perspective, this is effectively an update occurring in another part of the application.
The easiest way to fix this problem is to replace posts.find_by(id: event.target.data[:post_id])
with something like posts.find { |post| post.id.to_s == event.target.data[:post_id] }
. The key difference is that find
will use the existing Post
instance inside of @posts
instead of fetching a new one from the database. Since this instance is a part of your component's state, it will automatically re-render when it changes (and your template will see the correct, updated version when it reads posts
)
Another way to solve this problem is to make the component responsive to changes occurring in your data-model regardless of where or how posts are updated in your application. For this, you can use broadcasts. In this case, I would add an after_commit
to Post
that sends a broadcast when a post is updated (or perhaps just if: :saved_change_to_date?
if you want to be thrifty). Then, in your component, you can listen for that broadcast and reload
your relation.
tl;dr Motion will re-render when there are changes to a component's state. In your component, @posts
is not changed after you update the database, so it doesn't re-render. To fix this, you can either update through @posts
directly, or reload it in response to the underlying posts being changed elsewhere with broadcasts.
If you run into trouble getting this sorted out, let me know, and I am happy to open a PR against your demo application. 🙂
Since I think this is not a bug in Motion, I am going to go ahead and close this issue for now.
@davidalejandroaguilar Please feel free to continue the discussion here if you would like further clarification or think there is something I have missed.