初投稿になります。よろしくお願いします。
##この記事の目的
React、Rails間でのファイルアップロードに手こずったので同じ悩みを抱えている人のため記事を書くことにしました。
axiosを用いてReactからrailsに画像データを含むFormDataを送信し、rails側で保存した画像ファイルをReact側で参照できるようになることが目的です。
今回は、タイトルと本のラベル(画像)を登録し、idを入力するとデータが参照できるといった簡単なアプリケーションを作成していきます。
機能の実装に集中して解説をしたいので、スタイルはあまり当てません。
##開発環境について
自分のOSはwindowです。
検証はしていないですが、windows以外でも差し支えないと思われます。
##プロジェクトの作成
プロジェクトの作成については、rials newとcreate-react-appでパパっと済ませました。
reactはtypescriptで書いていきます。
####rails
> rails new backend
####React
> npx create-react-app frontend --template typescript
##必要なパッケージのインストール
####React
@typesのインストールが済んでいる方はaxiosだけで大丈夫です。
>cd frontend
> npm install --save axios @types/node @types/jest @types/react @types/react-dom
####rails
画像アップローダーにはcarrierwaveを使用します。
また、reactとrailsはクロスオリジンになるので、CORS設定用のgemもインストール。
# 略
gem 'rack-cors'
gem 'carrierwave'
> cd backend
> bundle install
##CORS設定
railsとreactのデフォルトのポート番号が3000番でかぶっているため、railsのポート番号を8000に変更
#略
port ENV.fetch("PORT") { 8000 }
続いて、config/initializersにcors.rbを作成し、以下のように記述すればCORS設定は完了です。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins "http://localhost:3000" #CROSを許可するオリジンのURL
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
##バックエンド
####modelの作成
labelカラムには画像のパスだけを保存することになるので、型はstringになります。
> rails g model book title:string label:string
> rails db:migrate
####uploaderの作成と、labelカラムとの紐づけ
carrierwaveをインストールしたので、uploaderが使えるようになりました。
labelアップローダーを作成します。
> rails g uploader label
以下の記述をすると、ラベルカラムに保存したパスから画像ファイルを参照することができるようになります。
class Book < ApplicationRecord
mount_uploader :label, LabelUploader #ラベルカラムとラベルアップローダーを紐づける
end
####ルーティング
Rails.application.routes.draw do
post '/books', to: 'books#create'
get '/books/:id', to: 'books#show'
end
####コントローラーの編集と作成
applicationコントローラーを以下のように編集します。
認証トークンを使用しないという記述です。
class ApplicationController < ActionController::Base
skip_before_action :verify_authenticity_token
end
booksコントローラーを作成します。
> rails g controller books
データはjsonで返します。
class BooksController < ApplicationController
def create
@book = Book.new(books_params)
if @book.save
render json: { status: 200, book: @book }
else
render json: { status: 500 }
end
end
def show
@book = Book.find(params[:id])
if @book
render json: { status: 200, book: @book }
else
render json: { status: 500}
end
end
private
def books_params
params.require(:book).permit(:title, :label)
end
end
##フロントエンド
とりあえず、srcディレクトリ内の
App.css App.test.tsx index.css logo.svg reportWebVitals.ts setupTest.ts
は削除します。気にならないようならそのままでも構いません。
index.tsxとApp.tsxも以下のようにお掃除。
import React from 'react';
const App: React.FC = () => {
return (
<div>
</div>
);
}
export default App;
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root')
);
####CreateBookコンポーネントの作成
ひとまず、フォームを作っていきます。
ファイルデータの型はFileなので、useState<File>()とします。
import React, { useCallback, useState } from 'react'
const CreateBook: React.FC = () => {
const [title, setTitle] = useState('')
const [label, setLabel] = useState<File>()
const selectImage = useCallback((e) => {
const selectedImage = e.target.files[0]
setLabel(selectedImage)
}, [])
const sendFormData = () => {
// formdataをrails側に送信する処理
//後で記述します。
}
return (
<div>
<label>タイトル:<input type="text" value={title} onChange={(e) => setTitle(e.target.value)}/></label>
<input type="file" onChange={(e) => selectImage(e)}/>
<button onClick={sendFormData}>送信</button>
</div>
)
}
export default CreateBook
ここまで来たらApp.tsxにCreateBook.tsxをインポートして、表示してみます。
> cd frontend
> yarn start
質素ですね(笑)。
####FormDataの作成とrailsへのデータの送信
追加した関数やモジュールのインポートには、addとコメントしてあります。
大事なコードにはポイント!とコメントしました。下に簡易的な解説を書いておきます。
import React, { useCallback, useState } from 'react'
import axios from 'axios' //add
const CreateBook: React.FC = () => {
const [title, setTitle] = useState('')
const [label, setLabel] = useState<File>()
const selectImage = useCallback((e) => {
const selectedImage = e.target.files[0]
setLabel(selectedImage)
}, [])
const createFormData = () => { //add
const formData = new FormData()
if (!label) return //labelがundefinedの場合早期リターン
formData.append('book[title]', title) // ポイント1!
formData.append('book[label]', label) // ポイント1!
return formData
}
const sendFormData = async () => { // ポイント2!
const url = 'http://localhost:8000/books'
const data = await createFormData() //formdataが作成されるのを待つ
const config = {
headers: {
'content-type': 'multipart/form-data'
}
}
axios.post(url, data, config)
.then(response => {
console.log(response)
}).catch(error => {
console.log(error)
})
}
return (
<div>
<h1>書籍を登録</h1>
<label>タイトル:<input type="text" value={title} onChange={(e) => setTitle(e.target.value)} /></label>
<input type="file" onChange={(e) => selectImage(e)}/>
<button onClick={sendFormData}>送信</button>
</div>
)
}
export default CreateBook
・ポイント1の解説
appendする際に以下のように普通に書いてしまうと、rails側でエラーになります。
パラメータを正しく受け取れないっぽいです。
ここで結構詰まりました。
formData.append('title', title)
・ポイント2の解説
createFormData()の処理の完了を待たないと、新しいformDataが作成されないままrails側にデータを送ってしまい、正しいデータが送信できません。
なのでここは非同期処理にしました。
では、フォームにタイトルと画像を入力して送信してみます。
コンソールにこのようなデータが帰ってきていたら成功です。
######ShowBookコンポーネントの作成
railsからは、railsから見た画像のURLしか送られてこないので、画像を表示するには画像のパスの前にrials側のパスを書いてやるに必要があります。
import React, { useState } from 'react'
import axios from 'axios'
const ShowBook: React.FC = () => {
const [bookId, setBookId] = useState('')
const [bookTitle, setBookTitle] = useState('')
const [labelUrl, setLabelUrl] = useState('')
const getBookUrl = () => {
if (!bookId) return
const url = `http://localhost:8000/books/${bookId}`
axios.get(url)
.then(response => {
const url = response.data.book.label.url
setLabelUrl(`http://localhost:8000/${url}`)
const title = response.data.book.title
setBookTitle(title)
})
}
return (
<div>
<h1>書籍を検索する</h1>
<input type="number" value={bookId} onChange={(e) => setBookId(e.target.value)}/>
<button onClick={getBookUrl}>検索</button>
<p>タイトル:{bookTitle}</p>
{labelUrl &&
<img src={labelUrl} alt="book label" width={200}/>
}
</div>
)
}
export default ShowBook
このようになっていたら成功です。
##最後に
存在しないidを入力するとエラーになったりといろいろなバグはありますが、今回の目的は’axiosを用いてReactからrailsに画像データを含むFormDataを送信し、rails側で保存した画像ファイルをReact側で参照できるようになること’なのでここで終わりにします。
formDataの作り方と画像の表示の仕方がわかればいろいろとカスタマイズできると思いますのでそのあたりは皆さんに任せたいと思います。
初投稿で拙い部分もあると思いますがお役に立てば幸いです。