💬

Rails いいね機能の非同期通信化(Ajax)

2023/08/01に公開

開発環境

ruby 3.1.2p20
Rails 6.1.7.4
Cloud9

前提

・Userモデル、Bookモデル、Favoriteは作成済み
・Devise導入済み

非同期通信とは

例えば、

<%= link_to book_favorites_path(book), method: :delete do %>
    <i class="fas fa-heart" aria-hidden="true" style="color: red;"></i>
    <%= book.favorites.count %> いいね
<% end %>

このようなコードがある時。
いいねボタンをクリックすると、ページがリロードされて、新しいページが作成されます。
このように、ページ全体がリロードされて、新しいページが作成される状態のことを、同期通信といいます。

Rails6の場合、デフォルトで同期通信状態になっています。

逆に非同期通信(Ajax)は、データの送信をした際にページをリロードしないで、変更のあった部分だけを取り急ぎ、JavaScriptで書き換えています。

「いいね」や「コメント」をする度にページをリロードするのは不便なので、非同期通信を使うようにします。

step1 jQueryの読み込み

非同期通信を行うときは、JacaScript と jQuery を使用します。
まずはjQueryを読み込みます。

$ yarn add jquery

上記コマンドを実施します。

次に、config/webpack/environment.js に以下のコードを記述します。

config/webpack/environment.js
const { environment } = require('@rails/webpacker')

// 追加ここから
const webpack = require('webpack')
environment.plugins.prepend('Provide',
  new webpack.ProvidePlugin({
    $: 'jquery/src/jquery',
    jQuery: 'jquery/src/jquery'
  })
)
// 追加ここまで

module.exports = environment

最後に、app/javascript/packs/application.js に以下を追記します。

app/javascript/packs/application.js
require("jquery")  //このコードを追加

これで準備が整いました。

step2 いいねボタンにAjaxの処理を適用

次に、いいねボタン部分にAjaxの処理を適用させます。

views/books/show.html/erb
...
<% if book.favorited_by?(current_user) %>
  <%= link_to book_favorites_path(book), method: :delete, remote: true, data: {"turbolinks" => false} do %> #ココ
    <i class="fas fa-heart" aria-hidden="true" style="color: red;"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% else %>
  <%= link_to book_favorites_path(book), method: :post, remote: true, data: {"turbolinks" => false}  do %> #ココ
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% end %>
...

上記コードのうち、link_to に含まれる
remote: true
data: {"turbolinks" => false}
の2つを追記しました。

remote: true を追記することで、非同期通信状態になります。

また、data: {"turbolinks" => false} を追記して、
限定的に turbolinks が動かないようにしています。
この記述がない場合、リロードしないと処理が表示されません。

step3 いいね保存/削除のリダイレクトを削除

favorites_controller.rb
class FavoritesController < ApplicationController
  def create
    @book = Book.find(params[:book_id])
    favorite = current_user.favorites.new(book_id: @book.id)
    favorite.save
    # redirect_to request.referer
  end

  def destroy
    @book = Book.find(params[:book_id])
    favorite = current_user.favorites.find_by(book_id: @book.id)
    favorite.destroy
    # redirect_to request.referer
  end
end

favoritesコントローラのcreate, destroyともに
redirect_to request.referer を使用していましたが、
この部分をコメントアウトします。

そうすることで、
リダイレクト先がない状態 & 非同期通信(step2) になり、
createアクション実行後は、create.js.erb を
destroyアクション実行後は destroy.js.erb を探すようになります。

step4 いいねボタンのテンプレート化

非同期通信で更新したい部分(いいねボタン)をテンプレート化します。

favorites/_btn.html.erb
<% if book.favorited_by?(current_user) %>
  <%= link_to book_favorites_path(book), method: :delete, remote: true, data: {"turbolinks" => false} do %>
    <i class="fas fa-heart" aria-hidden="true" style="color: red;"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% else %>
  <%= link_to book_favorites_path(book), method: :post, remote: true, data: {"turbolinks" => false}  do %>
    <i class="fas fa-heart" aria-hidden="true"></i>
    <%= book.favorites.count %> いいね
  <% end %>
<% end %>

step5 テンプレート読み込み部分のid設定

step4 で作成したテンプレートを読み込む際、注意点があります。
下記のように、読み込む部分には idを設定してください。
このidは step6で使用します。

views/books/show.html/erb
...
<table class='table'>
  <tr>
    <td><%= link_to(@book.user) do %>
      <%= image_tag @book.user.get_profile_image, size:"100x100" %><br>
      <%= @book.user.name %>
      <% end %>
    </td>
    <td><%= link_to @book.title, @book %></td>
    <td><%= @book.body %></td>
    <td>コメント数: <%= @book.book_comments.count %></td>
    <td id="favorite_buttons_<%= book.id %>"> #この部分
      <%= render "favorites/btn", book: @book %>
    </td>
    <% if @book.user == current_user %>
    <td><%= link_to 'Edit', edit_book_path(@book), class: "btn btn-sm btn-success edit_book_#{@book.id}" %></td>
    <td><%= link_to 'Destroy', @book, method: :delete, data: { confirm: '本当に消しますか?' }, class: "btn btn-sm btn-danger destroy_book_#{@book.id}"%></td>
    <% end %>
  </tr>
</table>
...

上記の

<td id="favorite_buttons_<%= book.id %>"> #この部分
  <%= render "favorites/btn", book: @book %>
</td>

この部分のように、_favorites/btn.html.erb を読み込む部分に idを設定します。

step6 JSファイルの作成

最後に、js.erbファイルを作成します。
step3 で削除したリダイレクトの代替部分です。

views/favorites/create.js.erb
$('#favorite_buttons_<%= @book.id %>').html("<%= j(render "favorites/btn", book: @book) %>");
views/favorites/destroy.js.erb
$('#favorite_buttons_<%= @book.id %>').html("<%= j(render "favorites/btn", book: @book) %>");

$('#favorite_buttons_<%= @book.id %>')
この部分は、jQueryのセレクターです。後半に書かれている処理は、#favorite_buttons_<%= @book.id %> に適用されます。

.html()
この部分はjQueryのメソッドです。引数に書かれたhtmlコンテンツを設定しています。

"<%= j(render "favorites/btn", book: @book) %>"
これはRubyのコードをJavaScriptの文字列中に埋め込んでいます。

jは escape_javascript と同じ意味です。
escape_javascript とは、調べてみてもなんだかよくわかりませんでしたが、、、
「RubyのコードをJavaScript中に書くときに、そのままだとうまくいかないことがあるが、
 escape_javascript を使っておけばいい感じに変換してくれるので安心」みたいな感じでした。
https://qiita.com/P-man_Brown/items/252e23892229b61575ab

render "favorites/btn", book: @book
これはよく見るパーシャルテンプレートの読み込みです。

確認すべきはログとコンソール部分

非同期通信(Ajax)処理を使い始めると、プレビュー画面にエラー文が表示されないことが増えます。
動作がうまくいかない場合は、ターミナルのログや、開発ツールの Console部分を確認するとエラーが出ていることがあります。

参照

https://qiita.com/hapiblog2020/items/3ba7e7edc02f01d987b9
https://zenn.dev/goldsaya/articles/1e20e5915d15af
https://qiita.com/RIN_HM/items/bdbd76f5015b3c15bfe9

Discussion