APIモードのRailsでActive StorageとS3を使ってバックエンド、Reactでフロントエンドを実装したい

作り終わってから思い出して書き始めたので抜けがあるかもしれない

バックエンド(Rails API

  • APIモードでRailsアプリを作ってCORSの設定とかをいい感じにする
  • aws-sdk-s3とactive_model_serializersをGemfileに追加してbundle install
  • S3にバケットを作ってパブリックアクセス(get)できるようにする。 qiita.com

  • /config/storage.ymlにS3使うためにservice、access_key_id、secret_access_key、region、bucketの指定をする qiita.com

  • $ rails active_storage:installして$ rails db:migrateする

    qiita.com

  • Userモデルを作る

  • /app/models/user.rbにhas_one_attached :avatarを追加する qiita.com

  • /config/environments/development.rbと/config/environments/production.rbと/config/environments/test.rbにconfig.active_storage.service = :amazonを追加(開発環境ではS3じゃなくてローカルに保存したいとかならここで変更)

    railsguides.jp

  • /app/serializers/user_serializer.rbにUserのSerializerを実装する。以下のavatarメソッドはlocalに保存する設定のときは失敗するので要注意。

 #例
 class UserSerializer < ActiveModel::Serializer
  attributes %i[id name avatar]

  def avatar
    object.avatar.attachment.service.send(:object_for, object.avatar.key).public_url if object.avatar.attached?
  end
end

qiita.com

  • ルーティングを追加 post '/users/current/avatar', to: 'users#update_avatar

  • UsersControllerを作ってUserのavatarを更新するためのメソッドを作る

# 例
  def update_avatar
    @user.avatar = params[:avatar]
    if @user.save
      render json: @user, status: :ok
    else
      render json: @user.errors.full_messages, status: :bad
    end
  end

sato-s.github.io

  • あとは適当に必要なAPIを作る。

フロントエンド(React+Redux)

  • アップロードページの例
import React from "react";
import { useDispatch } from "react-redux";
import { upload_user_avatar } from "../../actions/user";

const MyPage = props => {
  const file_input = React.useRef();
  const dispatch = useDispatch();
  const handle_submit = e => {
    e.preventDefault();
    if (
      file_input &&
      file_input.current &&
      file_input.current.files &&
      file_input.current.files.length > 0
    ) {
      const submit_data = new FormData();
      submit_data.append("avatar", file_input.current.files[0]);
      dispatch(upload_user_avatar(submit_data));
    }
  }
  
  return (
    <form onSubmit={handle_submit}>
      <input type="file" ref={file_input} accept="image/*" />
      <input type="submit" />
    </form>
  );
}
  • actionの例
export const UPLOAD_USER_AVATAR_REQUEST = "UPLOAD_USER_AVATAR_REQUEST";
export const UPLOAD_USER_AVATAR_SUCCESS = "UPLOAD_USER_AVATAR_SUCCESS";
export const UPLOAD_USER_AVATAR_FAILURE = "UPLOAD_USER_AVATAR_FAILURE";

export const upload_user_avatar = submit_data => async dispatch => {
  dispatch({ type: UPLOAD_USER_AVATAR_REQUEST });
  setAuthKeys();

  try {
    const res = await axios.post(
      `${config.backend_api_url}/users/current/avatar`,
      submit_data,
      {
        headers: {
          "content-type": "multipart/form-data"
        }
      }
    );
    return dispatch({ type: UPLOAD_USER_AVATAR_SUCCESS, res });
  } catch (error) {
    dispatch({ type: UPLOAD_USER_AVATAR_FAILURE, error });
  }
};

qiita.com

  • reducerの例
import {
  UPLOAD_USER_AVATAR_REQUEST,
  UPLOAD_USER_AVATAR_SUCCESS,
  UPLOAD_USER_AVATAR_FAILURE,
} from "../actions/user";

const initialState = {};

export default (state = initialState, action) => {
  switch (action.type) {
    case UPLOAD_USER_AVATAR_REQUEST:
      return { ...state };
    case UPLOAD_USER_AVATAR_SUCCESS:
      return { ...state, user: action.res.data.user };
    case UPLOAD_USER_AVATAR_FAILURE:
      return { ...state, error: action.error };
    default:
      return { ...state };
  }
};