3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React * Rails API + CarrierWave

Posted at

carrierwaveを使った画像アップロード with React

画像アップロードのスタートラインにやっと立てた感覚

それでも達成感がえぐいです。

要件

簡単な投稿CRUDアプリを想定。

投稿作成画面で投稿とそれに紐づく画像を1枚作成でき、投稿一覧にそれぞれの画像を表示させる

一覧、投稿、更新、削除ができるようにする

Postモデルはtitle,content,imageカラムを持つ

Rails

gem carrierwaveをインストールし、アップローダーを作成。今回はImageUploader

class Post < ApplicationRecord
  mount_uploader :image, ImageUploader
end

次にいつものようにコントローラーを作成。index, createを抜粋

class PostController < ApplicationController

  def index
    posts = Post.order(created_at: :desc)
    render json: posts
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post
    else
      render json: post.errors
    end
  end

  def update
    post = Post.find(params[:id])
    if post.update(post_params)
      render json: post
    else
      render json: post.errors
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy!
    render json: post
  end

  private

  def post_params
    params.require(:post).permit(:title, :content, :image)
  end
end

ルーティング

root to: "posts#index"
resource :posts, only: %i[index create update destroy]

最後にrailsで使用するポートを3001に変更する(3000以外ならなんでも良い。Reactも3000番ポートを使用するため)

## config/puma.rb

port ENV.fetch("PORT") { 3001 }

React * TypeScript

今回作成するコンポーネントは....

  • AllPosts: 投稿一覧ページ
  • PostCard: それぞれの投稿をカード形式で表示するコンポーネント
  • NewPost: 投稿作成ページ

*必要なライブラリーはインストール済みであるとする

NewPost

投稿作成ページであるNewPost.txsコンポーネントを作成

import { Heading, FormControl, FormLabel, Input, Box, Button, Stack } from "@chakra-ui/react";
import { ChangeEvent, memo, useState, VFC, } from "react";
import { useHistory } from "react-router-dom";
import axios from 'axios'

export const NewPost: VFC = memo(() => {

  const [newPostTitle, setNewPostTitle] = useState("")
  const [newPostContent, setNewPostContent] = useState("")
  const [image, setImage] = useState<File | >(null)

  const history = useHistory()

  const handlePostTitle = (e: ChangeEvent<HTMLInputElement>) => setNewPostTitle(e.target.value)
  const handlePostContent = (e: ChangeEvent<HTMLInputElement>) => setNewPostContent(e.target.value)
  const selectImage = (e: any) => {
    const selectedImage = e.target.files[0]
    setImage(selectedImage)
  }

  const createFormData = () => {
    const formData = new FormData()
    if (!image) return
    formData.append('post[title]', newPostTitle)
    formData.append('post[content]', newPostContent)
    formData.append('post[image]', image)
    return formData
  }

  const sendFormData = () => {
    const url = 'http://localhost:3001/posts'
    const data = createFormData()
    axios.post(url, data)
    .then(() => history.push('/'))
    .catch(e => {
      console.error(e)
    })
  }

  return (
    <>
      <Heading as="h1" color="teal" textAlign="center">NEW POST</Heading>
      <Box ml={6}>
        <Stack>
          <FormControl>
            <FormLabel>Title</FormLabel>
            <Input type="text" placeholder="some content" value={newPostTitle} onChange={handlePostTitle} />
          </FormControl>
          <FormControl>
            <FormLabel>Content</FormLabel>
            <Input type="text" placeholder="some content" value={newPostContent} onChange={handlePostContent} />
          </FormControl>
          <FormControl>
            <FormLabel>File</FormLabel>
            <Input type="file" onChange={(e) => selectImage(e)} />
            <Button onClick={sendFormData} bg="teal" color="white" mt={5}>Create</Button>
          </FormControl>
        </Stack>
      </Box>
    </>
  )
})

FormDataを作成し、対応するカラムに対応する値を格納し、パラメーターとして送る

AllPosts

投稿一覧を表示するコンポーネント

import { memo, useCallback, useEffect, useState, VFC } from "react";
import axios from 'axios'
import { PostCard } from "./PostCard";
import { Heading, Box, Wrap, WrapItem } from '@chakra-ui/react'

type Post = {
  id: number
  title: string
  content: string
  created_at: string
  image: {
    url: string
  }
}

export const AllPosts: VFC = memo(() => {

  const [posts, setPosts] = useState<Array<Post> | null>(null)

  const getAllPosts = useCallback(() => {
    axios.get('http://localhost:3001/posts')
      .then(res => {
        setPosts(res.data)
      })
      .catch((e) => console.error(e))
  },[])

  useEffect(() => {
    getAllPosts()
  }, [])

  return (
    <>
      <Box textAlign="center">
        <Heading color="gray.600">ALL POSTS</Heading>
      </Box>
      <Wrap p={{ base: 4, md: 10}}>
        {posts?.map((post) => (
          <WrapItem key={post.id}>
            <PostCard id={post.id} postTitle={post.title} postContent={post.content} postCreatedAt={post.created_at} imageUrl={post.image.url} getAllPosts={getAllPosts} />
          </WrapItem>
        ))}
      </Wrap>
    </>
  )
})

getAllPosts()...PostCard内で削除アクションを行った際に再度全ての投稿を表示させるための関数。propsとして渡すためuseCallbackを使ってみた

imageUrlとしてpost.image.urlを渡すことが味噌でした!

PostCard

import { memo, VFC } from "react";
import { Box, Image, Badge, Button, Flex } from '@chakra-ui/react'
import axios from 'axios'

type Props = {
  postTitle: string
  postContent: string
  postCreatedAt: string
  id: number
  imageUrl: string
  getAllPosts: () => void
}

export const PostCard: VFC<Props> = memo((props) => {

  const { postTitle, postContent, postCreatedAt, id, imageUrl, getAllPosts } = props

  const editPost = () => {
    alert("edit post")
  }

  const deletePost = (id: number) => {
    const sure = window.confirm("are you sure?")
    if (sure) {
      axios.delete(`http://localhost:3001/posts/${id}`)
      .then(() => {
        getAllPosts()
      })
      .catch(e => console.error(e))
    }
  }

  return (
    <Box maxW="sm" borderWidth="1px" borderRadius="lg" overflow="hidden">
      <Image style={{ width: "100%", height: "160px"}} src={imageUrl} alt="dummt image" />

      <Box p="6">
        <Box display="flex" alignItems="baseline">
          <Badge borderRadius="full" px="2" colorScheme="teal">
            {postTitle}
          </Badge>
          <Box
            color="gray.500"
            fontWeight="semibold"
            letterSpacing="wide"
            fontSize="xs"
            textTransform="uppercase"
            ml="2"
          >
            {postCreatedAt}
          </Box>
        </Box>

        <Box
          mt="1"
          fontWeight="semibold"
          as="h4"
          lineHeight="tight"
          isTruncated
        >
          {postContent}
        </Box>

        <Flex justifyContent="center">
          <Box mr={6}>
            <Button onClick={editPost} bg="teal" color="white">Edit</Button>
          </Box>
          <Box>
            <Button onClick={() => deletePost(id)} bg="red" color="white">Delete</Button>
          </Box>
        </Flex>

      </Box>
    </Box>
  )

})
3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?