Table of Contents
いいね機能とは
本記事で作るいいね機能は、以下のようなものです。
FacebookやTwitterにあるような気に入った投稿に好意的な気持ちを示すボタンを作る、ということです。 クリック(またはタップ)するとボタンの色やデザインが変わって、いいね済みであることが視覚的にわかります。
Reactコンポーネントを埋め込む理由
いいね機能自体はRails(+Vanilla JS)やjQueryで簡単に実装することができますが、本記事ではあえてReactコンポーネントとして実装します。 この方式には次のようなメリットがあると考えています。
フロントエンドの保守性を上げる
Vanilla JS、jQueryはHTML/CSS/JavaScriptを別々のファイルに記述するケースが多く、規模が大きくなると保守が難しくなります。
Reactコンポーネントであれば、1つのファイルにHTML/CSS/JavaScriptを記載することになるので、保守や流用がしやすくなります。
SPA化に向けた布石を打てる
既にあるシステムをSPA化したいといったシチュエーションで、一気に全ページをSPA化するのはリスクが高いです。
まずはRailsのViewを部分的にReactコンポーネント化し、Reactの資産が溜まった段階でSPA化を進めると、スムーズに移行することができます。
Reactの資産を活用できる
世の中には既にReactで実装されたパッケージやコンポーネントが多数公開しています。
Railsを使うことでスピーディに開発を進めつつ、Reactを使ったリッチなUIを実現しやすくなります。
使用するバージョン
- ruby 2.7.1
- Rails 6.0.3.4
- node v14.14.0
バックエンド(Rails)
念のため、1から作る前提で記載します。既に実装済の手順は飛ばして構いません。
rails new、deviseでログイン機能を追加
まずは rails new
し、必要なgemを追加します
$ rails new react_on_rails_likes
$ cd react_on_rails_likes
# gemを追加
$ bundle add devise
$ bundle add react_on_rails
# deviseをインストール
$ rails g devise:install
$ rails g devise user
$ rails db:migrate
app/controller/application_controller.rb
に以下を追記します。
class ApplicationController < ActionController::Base
before_action :authenticate_user! # 追記
end
Railsサーバを起動して http://localhost:3000/users/sign_in にアクセスすると、ログイン画面が表示されます。
# Railsサーバを起動
$ rails s
投稿機能をscaffold
Railsのscaffoldを活用して、一気に作ります。
# モデルを生成
$ rails g scaffold posts message:text
$ rails db:migrate
# Railsサーバを起動
$ rails s
http://localhost:3000/posts にアクセスすると投稿されたメッセージの一覧が表示されます。
いいね/いいねのキャンセル用のエンドポイントを追加
「いいね」は favorites
というテーブルに永続化する方針でモデル・コントローラを構築します。
# モデルを生成
$ rails g model favorites post:references user:references
$ rails db:migrate
# コントローラを生成
$ rails g controller favorites create destroy --skip-template-engine --skip-assets --skip-helper
app/models/user.rb
に has_many
を追加しておきます。
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :favorites # 追加
end
同様に、 app/models/post.rb
にも has_many
を追加します。
また、特定のユーザがいいね済みかどうか判定するメソッドも追加しておきましょう。
class Post < ApplicationRecord
has_many :favorites, dependent: :destroy # 追加
def favorite_by(user)
favorites.find{|f| f.user_id == user.id}
end
end
同じユーザが何度もいいねできないよう、 app/models/favorite.rb
にて一意性チェックを追加します。
class Favorite < ApplicationRecord
belongs_to :post
belongs_to :user
validates :post_id, uniqueness: { scope: :user_id }
end
config/routes.rb
を次のように修正します。
Rails.application.routes.draw do
resources :posts
resources :favorites, only: [:create, :destroy] # 追加
devise_for :users
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
end
app/controllers/favorites_controller.rb
を記述し、いいね/いいねのキャンセル処理を実装します。
class FavoritesController < ApplicationController
def create
favorite = current_user.favorites.create!(favorite_params)
render json: favorite
end
def destroy
favorite = Favorite.find(params[:id])
favorite.destroy
render json: favorite
end
private
def favorite_params
params.require(:favorite).permit(:post_id)
end
end
フロントエンド(React)
react_on_railsをインストール
$ rails generate react_on_rails:install
$ bundle install
$ bundle exec rails webpacker:install:react
このままだと、react_on_railsが自動生成したHelloWorld.jsxが 「Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:1. You might have mismatching versions of React and the renderer (such as React DOM)」
というエラーを出すので、以下のように修正しておきます。
import PropTypes from 'prop-types';
import React, { useState } from 'react';
const HelloWorld = (props) => {
const [name, setName] = useState(props.name);
return (
<div>
<h3>Hello, {name}!</h3>
<hr />
<form>
<label htmlFor="name">
Say hello to:
<input id="name" type="text" value={name} onChange={(e) => setName(e.target.value)} />
</label>
</form>
</div>
);
};
HelloWorld.propTypes = {
name: PropTypes.string.isRequired, // this is passed from the Rails view
};
export default props => <HelloWorld {...props} />; // ここを修正
# Railsサーバを起動
$ rails s
# 別のterminalでwebpack-dev-serverを起動
$ bin/webpack-dev-server
http://localhost:3000/hello_world にアクセスすると HelloWorld.jsx
を含む次のような画面が表示されます。
ReactコンポーネントにHTML/CSSを実装
いいねボタンのコンポーネントを作成して、Railsに埋め込めるようにします。
Bootstrap IconsをReactコンポーネントから利用できるよう、 react-bootstrap-icons
をインストールします。
$ yarn add react-bootstrap-icons
app/javascript/packs/bundle.js
を作成します。
import ReactOnRails from 'react-on-rails';
import Favorite from '../bundles/Favorite';
// This is how react_on_rails can see the HelloWorld in the browser.
ReactOnRails.register({
Favorite,
});
app/javascript/bundles/Favorite.jsx
を作成します。
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { HeartFill } from 'react-bootstrap-icons';
const Favorite = (props) => {
const { favoriteId } = props;
const [id, setId] = useState(favoriteId);
const clicked = () => {
console.log('clicked');
}
return (
<>
<HeartFill color={id ? 'red' : 'gray'} size={32} onClick={clicked}/>
</>
);
};
Favorite.propTypes = {};
export default props => <Favorite {...props} />;
app/views/layouts/application.html.erb
にてReactコンポーネントを含むJSを読み込みます。
<!DOCTYPE html>
<html>
<head>
<title>ReactOnRailsLikes</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'bundle' %> <%# ここを追加 %>
</head>
<body>
<%= yield %>
</body>
</html>
app/views/posts/index.html.erb
にてReactコンポーネントのレンダリングを追加します。
<p id="notice"><%= notice %></p>
<h1>Posts</h1>
<table>
<thead>
<tr>
<th>Message</th>
<th colspan="3"></th>
</tr>
</thead>
<tbody>
<% @posts.each do |post| %>
<tr>
<td><%= post.message %></td>
<td><%= link_to 'Show', post %></td>
<td><%= link_to 'Edit', edit_post_path(post) %></td>
<td><%= link_to 'Destroy', post, method: :delete, data: { confirm: 'Are you sure?' } %></td>
<%# 次の行を追加 %>
<td><%= react_component("Favorite", props: { postId: post.id, favoriteId: post.favorite_by(current_user)&.id }, prerender: false) %></td>
</tr>
<% end %>
</tbody>
</table>
<br>
<%= link_to 'New Post', new_post_path %>
N+1を防ぐために app/controllers/posts_controller.rb
も修正しておきましょう。
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :destroy]
# GET /posts
# GET /posts.json
def index
@posts = Post.eager_load(:favorites) # この行を修正
end
# 以下、省略
非同期処理(Ajax)を実装
ここまで来れば、あとはいいねボタンをクリックしたときの処理を実装するのみです。
import PropTypes from 'prop-types';
import React, { useState } from 'react';
import { HeartFill } from 'react-bootstrap-icons';
import axios from 'axios';
axios.defaults.headers['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const Favorite = (props) => {
const { postId, favoriteId } = props;
const [id, setId] = useState(favoriteId);
const clicked = async () => {
if (id == null) {
const { data: { id = null } } = await axios.post('/favorites', { post_id: postId })
id && setId(id);
} else {
await axios.delete(`/favorites/${id}`)
setId(null);
}
}
return (
<>
<HeartFill color={id ? 'red' : 'gray'} size={32} onClick={clicked}/>
</>
);
};
Favorite.propTypes = {};
export default props => <Favorite {...props} />;
バックエンドにRailsを使う都合上、 X-CSRF-TOKEN
をセットする必要がある点に注意してください。
あとはReactコンポーネント内の postId
の有無に応じてリクエスト先を切り替えればOKです。
まとめ
このようにReactコンポーネントとして、Reactの資産を増やしていくことでRailsとReactの両方のメリットを活かした開発が可能です。 みなさんも、ぜひお試しください。
Related Posts
Daiki Urata
2024/06/10
Daiki Urata
2023/05/22