0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Rails7] Turbo Streamsで実現するリアルタイムブックマーク機能

Last updated at Posted at 2025-02-16

はじめに

Rails7では、Turbo/HotwireRailsUJSの導入により、従来の方法とは異なる手法で非同期通信や動的なUI更新が可能になりました。本記事では、ブックマーク機能を実装する際に直面した問題とその解決策、成功の要因について詳しく解説します。

実装環境

  • 投稿のモデル:Tweetモデル
  • ユーザー情報のモデル:Userモデル
  • 中間モデル:Bookmarkモデル

実装の背景

Rails 7 への移行

Rails7ではTurbo/Hotwireがデフォルトで有効になり、リンクやフォームの送信方法が従来と異なります。そのため、今までと同じ実装方法ではうまくいかない場合がありました。
RailsUJSの設定やインポートが重要な役割を果たしており、これが正しく読み込まれていないとAJAXリクエストが正しく機能しませんでした。

ブックマーク機能の概要

ユーザーは各ツイートに対してブックマークを行い、ブックマークされたツイートの一覧を確認できるようにします。
非同期リクエスト(AJAX)により、ページ全体のリロードなしでアイコンの切替が可能になります。

主な実装工程

ルーティングの設定

config/routes.rbにおいて、ツイートごとにブックマーク作成・削除のルートを設定しました。

routes.rb
resources :tweets do
  resource :bookmark, only: %i[create destroy]
end

この設定により、POST /tweets/:tweet_id/bookmarkDELETE /tweets/:tweet_id/bookmarkのルートが生成されます。

コントローラーの実装

BookmarksControllerを作成し、ブックマークの作成と削除のアクションを実装しました。

bookmarks_controller.rb
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モデルに、ブックマーク用の関連付けとメソッドを実装しました。

user.rb
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)
tweets/index.html.erb
<%= render 'tweet', tweet: tweet %> #追記
_tweet.html.erb
<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>
_bookmark.html.erb
  <%= 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 %>
_unbookmark.html.erb
  <%= 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.erbheadタグの中にAdd Your Kit's Code to a Projectに書かれているコードを追加します。
スクリーンショット 2025-02-16 12.55.57.png

AJAXレスポンス用のJSテンプレート

bookmarks/create.js.erb
document.getElementById("js-bookmark-button-for-tweet-<%= @tweet.id %>").innerHTML = "<%= j(render 'bookmarks/unbookmark', tweet: @tweet) %>";
bookmarks/destroy.js.erb
document.getElementById("js-bookmark-button-for-tweet-<%= @tweet.id %>").innerHTML = "<%= j(render 'bookmarks/bookmark', tweet: @tweet) %>";

JavaScriptRailsUJSの設定

app/javascript/application.jsでは、RailsUJSを正しく読み込むことが重要でした。
application.jsがこのようになっているか確認してください

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に以下の記述があるかを確認する

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.erbheadタグに以下の記述があるか確認する

application.html.erb
<%= 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.erbdestroy.js.erbを作成し、ユーザーがリロードせずにアイコンの状態を即時更新できるようにしました。

ブックマーク一覧ページの実装

さらに、ユーザーがブックマークしたツイートだけを一覧表示するページも実装できます。

ルーティング

config/routes.rbにおいて、ツイートごとにブックマーク作成・削除のルートを設定しました。

routes.rb
resources :tweets do
    resource :bookmark, only: %i[create destroy]  
    collection do
      get 'bookmarks'
    end
  end

この設定により、POST/tweets/:tweet_id/bookmarkDELETE /tweets/:tweet_id/bookmarkのルートが生成されます。

コントローラーの実装

tweets_controller.rb
def bookmarks
  @tweets = current_user.bookmark_tweets
end

ビュー (app/views/tweets/bookmarks.html.erb)

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 テンプレートの実装
  • ルーティングやモデル、ビューの連携を細かく調整してトラブルシュートしたこと

これらを実装することで、リロードなしで状態更新できる動的なブックマーク機能を実現できました。

参考記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?