はじめに
この記事は、プログラミング初学者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。内容に不備などあればご指摘いただますと幸いです。
今回、Rails(API) x ReactでX(旧Twitter)のクローンサイトを作る際にツイート投稿機能を実装しました。
そのツイート投稿機能の実装方法について備忘録も兼ねて記事を作成していきます。
この記事では下記を解説しています。
- ツイート本文 + 複数画像を投稿
- ツイート本文のみの投稿
- Active Storage に複数画像を保存
実装の流れ
先にツイートを作成
↓
そのツイートに画像を紐づける
という 2段階方式 を実装していきます。
ActiveStorage のセットアップ
ActiveStorage インストール
下記のコマンドを実行します。
rails active_storage:install
rails db:migrate
画像保存用モデルを作成(Tweetモデル)
下記のコマンドを実行します。
rails g model tweet user:references content:text
rails db:migrate
Tweet モデルで ActiveStorage を紐付け
下記の行を追加します。
class Tweet < ApplicationRecord
belongs_to :user
has_many_attached :images # 追加
end
コントローラの作成(API)
下記のコマンドを実行します。
rails g controller api/v1/images
rails g controller api/v1/tweets
app/controllers/api/v1/tweets_controller.rbに以下を書きます。
module Api
module V1
class TweetsController < ApplicationController
before_action :authenticate_api_v1_user!
def create
tweet = current_api_v1_user.tweets.build(content: params[:content])
if tweet.save
render json: {
id: tweet.id,
content: tweet.content,
createdAt: tweet.created_at
}, status: :created
else
render json: tweet.errors, status: :unprocessable_entity
end
end
end
end
end
ポイント
-
current_api_v1_user.tweets.buildはtweet.user_id = current_api_v1_user.idを自動でセットした状態で Tweet を生成します
app/controllers/api/v1/images_controller.rbに以下を書きます。
module Api
module V1
class ImagesController < ApplicationController
before_action :authenticate_api_v1_user!
def create
# すでに作られているtweetを検索
tweet = Tweet.find(params[:tweet_id])
# 画像は複数想定
params[:images]&.each do |image|
tweet.images.attach(image)
end
render json: {
tweetId: tweet.id,
# url_for は Rails のヘルパーで、ActiveStorage の画像を実際にアクセスできるURLに変換する
images: tweet.images.map { |img| url_for(img) }
}, status: :created
end
end
end
end
ポイント
-
params[:images]に React から送られたファイル群が入ります -
eachで ActiveStorage に attach -
params[:images]&.eachは &. (ぼっち演算子) を使ってレシーバ(params[:images])が nil の場合に NoMethodError を発生させず、ただ nil を返すようにしています -
url_forで実際にアクセスできるURLに変換して返します(Reactで表示可能) -
tweet.images.map { |img| url_for(img) }はTweet に紐づく画像を “表示可能なURL” に変換して配列で返しています
ルーティングの設定
下記の行を追加します。
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
mount_devise_token_auth_for 'User', at: 'users', controllers: {
registrations: 'api/v1/registrations'
}
resources :tweets, only: %i[index show create destroy] # 追加
resources :images, only: %i[create] # 追加
end
end
end
CORSの設定
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:5173" # ReactサーバーのURL
resource "*", # すべてのエンドポイントに適用
headers: :any, # すべてのヘッダーを許可
expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'], #フロント側(React)に特定のレスポンスヘッダーを見えるようにする設定
methods: [:get, :post, :put, :patch, :delete, :options, :head] # 許可するHTTPメソッドを定義
end
end
React側
import axios from "axios";
const apiClient = axios.create({
baseURL: "http://localhost:3000/api/v1",
});
export default apiClient;
上記では、baseURLをセットしたaxiosインスタンスを作成して、以下のコンポーネント内で axios を使用する際のURLを簡略化しています。
Axios インスタンス
import { FaEarthAmericas } from "react-icons/fa6";
import { CiImageOn } from "react-icons/ci";
import { RiFileGifLine } from "react-icons/ri";
import { RiListRadio } from "react-icons/ri";
import { CiFaceSmile } from "react-icons/ci";
import { TbCalendarTime } from "react-icons/tb";
import { FiMapPin } from "react-icons/fi";
import PostButton from "../ui/PostButton";
import { useState } from "react";
import apiClient from "../../apis/apiClient";
const TweetForm = () => {
const [content, setContent] = useState("");
const [images, setImages] = useState([]);
const submitTweet = async (e) => {
e.preventDefault();
try {
// Tweet を作成
const res = await apiClient.post("/tweets", {
content: content,
});
const tweetId = res.data.id;
// 画像があれば紐づける
if (images.length > 0) {
const formData = new FormData();
// 複数画像を FormData に追加
images.forEach((img) => formData.append("images[]", img));
// TweetのIDを FormData に追加
formData.append("tweet_id", tweetId);
await apiClient.post("/images", formData);
}
setContent("");
setImages([]);
} catch (e) {
console.error(e);
}
};
return (
<div className="flex border-b border-gray-600 px-4 pt-1">
<div className="me-2 mt-3 h-10 w-10 rounded-full bg-white"></div>
<div className="flex-1 pt-1">
<form onSubmit={submitTweet}>
{/* ツイート投稿フォーム */}
<input
type="text"
name="content"
placeholder="いまどうしてる?"
value={content}
onChange={(e) => setContent(e.target.value)}
className="mx-0.5 mt-0.5 w-full py-3 text-xl focus:border-0 focus:outline-0"
/>
<div className="mt-1 flex items-center border-b border-gray-600 px-2 pb-3 text-sky-500">
<FaEarthAmericas size={13} />
<span className="ml-1 text-sm font-bold">全員が返信できます</span>
</div>
<div className="flex justify-between py-2">
<div className="flex items-center gap-4 px-1.5 text-sky-500">
{/* 画像投稿アイコン */}
<label>
<CiImageOn size={20} className="cursor-pointer" />
<input
type="file"
multiple
onChange={(e) => {
const images = Array.from(e.target.files);
setImages(images);
}}
className="hidden"
/>
</label>
<RiFileGifLine size={20} />
<RiListRadio size={20} />
<CiFaceSmile size={20} />
<TbCalendarTime size={20} />
<FiMapPin size={20} />
</div>
<PostButton size="small" />
</div>
</form>
</div>
</div>
);
};
export default TweetForm;
ポイント
- multiple で複数画像選択ができます
<input type="file" multiple />
-
Array.fromメソッドは、文字列や配列風オブジェクトから新しい配列を生成するメソッド -
FormDataオブジェクトは、ファイルアップロードを含む送信を簡潔に処理するのに便利なオブジェクト - フォームデータでは
images[]として送ります。
HTTP リクエストで配列を送る場合は、キー名に [] を付ける必要があります。
モデルにhas_many_attached :imagesと定義したことで、 Rails 側では配列を求めています。そのため、送る側も配列形式で送る必要があります。
おわりに
最後まで読んでいただきありがとうございました。
参考にしたサイト