はじめに
Rails7では、Turbo/Hotwire
やRailsUJS
の導入により、従来の方法とは異なる手法で非同期通信や動的なUI更新が可能になりました。本記事では、ブックマーク機能を実装する際に直面した問題とその解決策、成功の要因について詳しく解説します。
実装環境
- 投稿のモデル:
Tweet
モデル - ユーザー情報のモデル:
User
モデル - 中間モデル:
Bookmark
モデル
実装の背景
Rails 7 への移行
Rails7ではTurbo/Hotwire
がデフォルトで有効になり、リンクやフォームの送信方法が従来と異なります。そのため、今までと同じ実装方法ではうまくいかない場合がありました。
RailsUJS
の設定やインポートが重要な役割を果たしており、これが正しく読み込まれていないとAJAX
リクエストが正しく機能しませんでした。
ブックマーク機能の概要
ユーザーは各ツイートに対してブックマークを行い、ブックマークされたツイートの一覧を確認できるようにします。
非同期リクエスト(AJAX)により、ページ全体のリロードなしでアイコンの切替が可能になります。
主な実装工程
ルーティングの設定
config/routes.rb
において、ツイートごとにブックマーク作成・削除のルートを設定しました。
resources :tweets do
resource :bookmark, only: %i[create destroy]
end
この設定により、POST /tweets/:tweet_id/bookmark
とDELETE /tweets/:tweet_id/bookmark
のルートが生成されます。
コントローラーの実装
BookmarksController
を作成し、ブックマークの作成と削除のアクションを実装しました。
class BookmarksController < ApplicationController
before_action :authenticate_user!
def create
@tweet = Tweet.find(params[:tweet_id])
current_user.bookmark(@tweet)
respond_to do |format|
format.js # create.js.erb をレンダリング
format.html { redirect_to request.referer, notice: "ブックマークしました" }
end
end
def destroy
@tweet = Tweet.find(params[:tweet_id])
current_user.unbookmark(@tweet)
respond_to do |format|
format.js # destroy.js.erb をレンダリング
format.html { redirect_to request.referer, notice: "ブックマークを解除しました" }
end
end
end
モデルの設定
Userモデル
に、ブックマーク用の関連付けとメソッドを実装しました。
class User < ApplicationRecord
has_many :bookmarks, dependent: :destroy
has_many :bookmark_tweets, through: :bookmarks, source: :tweet
def own?(object)
id == object.user_id
end
def bookmark?(tweet)
bookmarks.exists?(tweet_id: tweet.id)
end
def bookmark(tweet)
bookmarks.create(user_id: id, tweet_id: tweet.id) unless bookmark?(tweet)
end
def unbookmark(tweet)
bookmarks.find_by(tweet_id: tweet.id)&.destroy
end
end
これにより、current_user.bookmark_tweets
でブックマーク済みのツイートを一覧取得可能です。
Tweetモデルのアソシエーションも忘れないようにしましょう。
ビューの実装とAJAX
更新
各ツイート表示の部分で、ブックマークの状態に応じてボタンを切り替えるために、部分テンプレートを利用しました。
以下のViewファイルを作成する
- tweets/_tweet.html.erb
- 未ブックマーク用(bookmarks/_bookmark.html.erb)
- ブックマーク済み用(bookmarks/_unbookmark.html.erb)
<%= render 'tweet', tweet: tweet %> #追記
<div class="tweet">
<% if user_signed_in? %>
<p><%= tweet.body %></p>
<% if current_user.bookmark?(tweet) %>
<%= render 'bookmarks/unbookmark', tweet: tweet %>
<% else %>
<%= render 'bookmarks/bookmark', tweet: tweet %>
<% end %>
<% end %>
</div>
<%= link_to tweet_bookmark_path(tweet),
method: :post,
remote: true,
data: { turbo: "false" },
id: "js-bookmark-button-for-tweet-#{tweet.id}" do %>
<i class="fa-regular fa-bookmark"></i>
<% end %>
<%= link_to tweet_bookmark_path(tweet),
method: :delete,
remote: true,
data: { turbo: "false" },
id: "js-bookmark-button-for-tweet-#{tweet.id}" do %>
<i class="fa-solid fa-bookmark"></i>
<% end %>
注意
今回、ブックマークアイコンはfontawesomeを使用しています。そのため、未登録の方はサインアップからお願いします。
登録後、「12a4b6...」というような文字列が並んでいますと思います。これはあなただけの秘密の暗号(APIキーと言います)なので、流出しないように気を付けましょう!
まず、fontawesome を使う事前準備として、application.html.erb
のhead
タグの中にAdd Your Kit's Code to a Project
に書かれているコードを追加します。
AJAXレスポンス用のJSテンプレート
document.getElementById("js-bookmark-button-for-tweet-<%= @tweet.id %>").innerHTML = "<%= j(render 'bookmarks/unbookmark', tweet: @tweet) %>";
document.getElementById("js-bookmark-button-for-tweet-<%= @tweet.id %>").innerHTML = "<%= j(render 'bookmarks/bookmark', tweet: @tweet) %>";
JavaScript
とRailsUJS
の設定
app/javascript/application.js
では、RailsUJS
を正しく読み込むことが重要でした。
application.jsがこのようになっているか確認してください
// Rails UJS を先に読み込む
import Rails from "@rails/ujs"
Rails.start()
// 次に Turbo を読み込む
import "@hotwired/turbo-rails"
import "controllers"
// もしTurboの自動駆動が邪魔する場合は無効化(ただし、data-turbo="false"をリンクに追加していれば不要な場合もあります)
Turbo.session.drive = false
RailsUJS が正しく動作していなければ、リンククリック時に正しい HTTP メソッドが送信されず、GET
リクエストになってしまいます。
ここまでやってGetメソッドになる
その場合、importmapが正しく使えていない可能性があります。以下を確認してみてください。
①config/importmap.rb
に以下の記述があるかを確認する
pin "@rails/ujs", to: "https://ga.jspm.io/npm:@rails/ujs@7.0.4/lib/assets/compiled/rails-ujs.js"
②application.html.erb
のhead
タグに以下の記述があるか確認する
<%= javascript_importmap_tags %>
課題と成功の要因
課題
- Rails7の新技術 (Turbo/Hotwire) の導入
- これにより、従来の
RailsUJS
の挙動が変わり、リンククリック時の HTTP メソッド指定に苦労しました。
- これにより、従来の
- JavaScriptモジュールの読み込み
-
@rails/ujs
が読み込まれていなかったため、非同期リクエストが正しく動作せず、GET
リクエストになってしまう問題が発生しました。
成功の要因
-
RailsUJS
の正しい設定-
import Rails from "@rails/ujs"
を適切に設定し、RailsUJS
を有効化することで、非同期リクエストが正常に動作するようになりました。
-
- Turbo の影響を無効化
- リンクに
data: { turbo: "false" }
を追加し、Turbo
がリンクのHTTP
メソッドを上書きしないようにしたこと。
- リンクに
-
AJAX
更新のためのJSテンプレートの実装-
create.js.erb
とdestroy.js.erb
を作成し、ユーザーがリロードせずにアイコンの状態を即時更新できるようにしました。
-
ブックマーク一覧ページの実装
さらに、ユーザーがブックマークしたツイートだけを一覧表示するページも実装できます。
ルーティング
config/routes.rb
において、ツイートごとにブックマーク作成・削除のルートを設定しました。
resources :tweets do
resource :bookmark, only: %i[create destroy]
collection do
get 'bookmarks'
end
end
この設定により、POST/tweets/:tweet_id/bookmark
とDELETE /tweets/:tweet_id/bookmark
のルートが生成されます。
コントローラーの実装
def bookmarks
@tweets = current_user.bookmark_tweets
end
ビュー (app/views/tweets/bookmarks.html.erb)
<h1>ブックマークしたツイート一覧</h1>
<% if @tweets.present? %>
<% @tweets.each do |tweet| %>
<%= render 'tweet', tweet: tweet %>
<% end %>
<% else %>
<p>まだブックマークしたツイートはありません。</p>
<% end %>
まとめ
今回の成功要因は以下の通りです.
- Rails 7 の新技術(Turbo/Hotwire)の理解と適切な設定
- Rails UJS の正しい読み込みと設定
- AJAX リクエストに対する JS テンプレートの実装
- ルーティングやモデル、ビューの連携を細かく調整してトラブルシュートしたこと
これらを実装することで、リロードなしで状態更新できる動的なブックマーク機能を実現できました。
参考記事