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