🌸

【Rails7】Turboを使って非同期でいいね機能を実装した話

2024/04/08に公開
自己紹介

もなかと申します。
プログラミング学習2年目の初心者です。
Ruby on Railsをメインに勉強しています。
実務は未経験です。
内容につきまして、間違いや不備がありましたらコメントで教えてください。

参考文献

https://zenn.dev/ganmo3/articles/c071ba9aecaa51
https://zenn.dev/shita1112/books/cat-hotwire-turbo/viewer/turbo-streams-fetch
https://qiita.com/NaokiIshimura/items/d91b10587881107b5f39

いいね機能の実装

環境

Rails 7.0.8
ruby 3.2.2

Gemfile
+ gem 'turbo-rails'

modelの作成

rails g model Favorite user_id:integer post_id:integer
rails db:migrate

モデルは下記の内容を追記します。

favorite.rb
+class Favorite < ApplicationRecord
+    belongs_to :user
+    belongs_to :post
+end
user.rb
class User < ApplicationRecord
+  has_many :favorites, dependent: :destroy
end
post.rb
class Post < ApplicationRecord
+ has_many :favorites, dependent: :destroy
  :
+ def favorited_by?(user)
+   favorites.exists?(user_id: user.id)
+ end
end

ルーティングの設定

いいねを作成したり、削除できるように、createとdestroyで設定します。
URLにidが表示されないようにするため、resourceを単数形で実装します。

  resources :posts do
+    resource :favorites, only: %i[create destroy]
  end

controllerの作成

rails g controller favorites
controllers/favorites_controller.rb
class FavoritesController < ApplicationController
    before_action :require_login

    def create
        post = Post.find(params[:post_id])
        favorite = current_user.favorites.new(post_id: post.id)
        respond_to do |format|
            if favorite.save
                format.turbo_stream do
                    render turbo_stream: turbo_stream.update("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to favorite.' }
            end
        end
    end

    def destroy
        favorite = current_user.favorites.find_by(post_id: params[:post_id])
        post = Post.find(params[:post_id])
        respond_to do |format|
            if favorite.destroy
                format.turbo_stream do
                    render turbo_stream: turbo_stream.update("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to unfavorite.' }
            end
        end
    end
end

viewの作成

posts/_favorite.html.erb
<% if logged_in? %>
    <% if post.favorited_by?(current_user) %>
        <%= link_to post_favorites_path(post), data: { turbo_method: :delete } do %>
            <i class="fas fa-heart text-lg" aria-hidden="true" style="color: red;"></i>
            <%= post.favorites.count %>
        <% end %>
    <% else %>
        <%= link_to post_favorites_path(post), data: { turbo_method: :post } do %>
            <i class="fas fa-heart text-lg" aria-hidden="true"></i>
            <%= post.favorites.count %>
        <% end %>
    <% end %>
<% else %>
<% end %>
views/posts/_post.html.erb
<div id="post_<%= post.id %>_favorite">
    <%= render "posts/favorite", post: post %>
</div>

起きた問題

1. いいねができない。

初めに_post.html.erbに下記のコードのみを加えたのですがうまく反映できませんでした。

views/posts/_post.html.erb
<%= render "posts/favorite", post: post %>

下記のようにdivタグで囲むことで実装ができました。

views/posts/_post.html.erb
+ <div id="post_<%= post.id %>_favorite">
    <%= render "posts/favorite", post: post %>
+ </div>

2. 非同期にならない。

初めはコントローラーを下記のように書いていました。

_favorites_controller.rb
class FavoritesController < ApplicationController
    before_action :require_login

    def create
        post = Post.find(params[:post_id])
        favorite = current_user.favorites.new(post_id: post.id)

        respond_to do |format|
            if favorite.save
                format.turbo_stream do
+                    render turbo_stream: turbo_stream.prepend("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to favorite.' }
            end
        end
    end

    def destroy
        favorite = current_user.favorites.find_by(post_id: params[:post_id])
        post = Post.find(params[:post_id])

        respond_to do |format|
            if favorite.destroy
                format.turbo_stream do
+                    render turbo_stream: turbo_stream.replace("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to unfavorite.' }
            end
        end
    end
end

prependは指定したTurboFrameの中の先頭に要素を追加します。
replaceは指定したTurboFrameを上書きします。
その結果、いいねボタンが増殖したり、ボタンを押して初めの1.2回は非同期で行えてもその後は画面上に反映されないトラブルが起きました。

そこで下記の記事のTurbo 7つのアクションを参考に修正することで思うように反映させることができました。
https://qiita.com/NaokiIshimura/items/d91b10587881107b5f39

favorites_controller.rb
# いいね機能についてのコントローラー
class FavoritesController < ApplicationController
    before_action :require_login

    def create
        post = Post.find(params[:post_id])
        favorite = current_user.favorites.new(post_id: post.id)

        respond_to do |format|
            if favorite.save
                format.turbo_stream do
+                    render turbo_stream: turbo_stream.update("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to favorite.' }
            end
        end
    end

    def destroy
        favorite = current_user.favorites.find_by(post_id: params[:post_id])
        post = Post.find(params[:post_id])

        respond_to do |format|
            if favorite.destroy
                format.turbo_stream do
+                    render turbo_stream: turbo_stream.update("post_#{post.id}_favorite", partial: 'posts/favorite', locals: { post: post })
                end
            else
                format.html { redirect_to post, alert: 'Failed to unfavorite.' }
            end
        end
    end
end

作成したアプリ

https://www.nanikiru-mahjong.com/
https://github.com/monaka0309/nanikiru

最後に

最後まで記事を見ていただきありがとうございます。
分かりにくいコードであったり、説明不足の部分もあったかと思います。
これからも勉学に励んでいきたいと思います。
誤り等あればコメントをお願いいたします。

Discussion