13
16

More than 3 years have passed since last update.

ReactからRailsにFormDataを送信してファイルをアップロードする

Last updated at Posted at 2021-03-07

初投稿になります。よろしくお願いします。

この記事の目的

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もインストール。

Gemfile
# 略
gem 'rack-cors'

gem 'carrierwave'
> cd backend
> bundle install

CORS設定

railsとreactのデフォルトのポート番号が3000番でかぶっているため、railsのポート番号を8000に変更

config/puma.rb
#略
port ENV.fetch("PORT") { 8000 }

続いて、config/initializersにcors.rbを作成し、以下のように記述すればCORS設定は完了です。

config/initializers/cors.rb
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

以下の記述をすると、ラベルカラムに保存したパスから画像ファイルを参照することができるようになります。

models/book.rb
class Book < ApplicationRecord
  mount_uploader :label, LabelUploader #ラベルカラムとラベルアップローダーを紐づける
end

ルーティング

config/routes.rb
Rails.application.routes.draw do
  post '/books', to: 'books#create'
  get  '/books/:id', to: 'books#show'
end

コントローラーの編集と作成

applicationコントローラーを以下のように編集します。
認証トークンを使用しないという記述です。

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token
end

booksコントローラーを作成します。

> rails g controller books

データはjsonで返します。

controllers/books_controller.rb

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も以下のようにお掃除。

src/App.tsx
import React from 'react';

const App: React.FC = () => {
  return (
    <div>
    </div>
  );
}

export default App;
src/index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
    <App />,
  document.getElementById('root')
);

CreateBookコンポーネントの作成

ひとまず、フォームを作っていきます。
ファイルデータの型はFileなので、useState<File>()とします。

scr/components/CreateBook
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

image.png
質素ですね(笑)。

FormDataの作成とrailsへのデータの送信

追加した関数やモジュールのインポートには、addとコメントしてあります。
大事なコードにはポイント!とコメントしました。下に簡易的な解説を書いておきます。

src/components/CreateBook.tsx
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側にデータを送ってしまい、正しいデータが送信できません。
なのでここは非同期処理にしました。

では、フォームにタイトルと画像を入力して送信してみます。
コンソールにこのようなデータが帰ってきていたら成功です。
image.png

ShowBookコンポーネントの作成

railsからは、railsから見た画像のURLしか送られてこないので、画像を表示するには画像のパスの前にrials側のパスを書いてやるに必要があります。

src/ShowBook.tsx
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を入力して書籍のデータを表示してみます。
image.png

このようになっていたら成功です。

最後に

存在しないidを入力するとエラーになったりといろいろなバグはありますが、今回の目的は’axiosを用いてReactからrailsに画像データを含むFormDataを送信し、rails側で保存した画像ファイルをReact側で参照できるようになること’なのでここで終わりにします。
formDataの作り方と画像の表示の仕方がわかればいろいろとカスタマイズできると思いますのでそのあたりは皆さんに任せたいと思います。

初投稿で拙い部分もあると思いますがお役に立てば幸いです。

13
16
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
13
16