React * Rails API + CarrierWave

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

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

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

## 要件

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

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

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

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

## Rails

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

```rb
class Post < ApplicationRecord
  mount_uploader :image, ImageUploader
end
```

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

```rb
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
```

ルーティング

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

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

```rb
## config/puma.rb

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

## React * TypeScript

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

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

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

### NewPost

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

```tsx
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

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

```tsx
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

```tsx
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>
  )

})
```