🐈

Rails API + Reactで中間テーブルを用いた複数タグ投稿

2022/02/12に公開

やったこと

前回の「Rails APIで中間テーブルを用いた複数タグ投稿」という記事で作ったAPIを用いて実際にReactで投稿フォームから投稿してDBにデータを保存してみました。

https://zenn.dev/shogo_matsumoto/articles/1dd51149d088e6

前回の記事で作ったAPIを使っているので、上の記事を読んでから今回の記事を読んでもらえるとわかりやすいと思います。

作るもの

  • react-tag-inputというパッケージを用いて複数のタグが投稿できる投稿フォームを作りました。
  • タグが重複してるものは削除してそれぞれ一つだけが保存されるようにしました。

手順

  1. プロジェクト作成
  2. 必要パッケージのインストール
  3. API通信設定
  4. コンポーネント作成
  5. ルーティング設定
  6. ページ作成

1. プロジェクト作成

任意のディレクトリに移動した後、Reactプロジェクトを作成します。
今回はcreate-react-appで作成していきます。

terminal
$ npx create-react-app frontend
  • create-react-appの後のプロジェクト名は任意の名前になります。今回はfrontendと指定しました。

必要パッケージのインストール

作成したReactプロジェクトに移動して、必要なパッケージをインストールしていきましょう。

まずはReactプロジェクトに移動します。

terminal
$ cd frontend

次に複数のタグを作成するためのパッケージをインストールしていきます。

terminal
$ npm i react-tag-input @pathofdev/react-tag-input/build/index.css
  • react-tag-input
    • 複数のタグを簡単に作成して配列に入れてくれる。
  • @pathofdev/react-tag-input/build/index.css
    • react-tag-inputのスタイルを整える。

次にAPI通信用のパッケージをインストールしていきます。

terminal
$ npm i axios axios-case-converter
  • axios
    • HTTP 通信を簡単にしてくれる。
  • axios-case-converter
    • axios で受け取ったレスポンスの値をスネークケース → キャメルケースに変換、または送信するリクエストの値をキャメルケース → スネークケースに変換してくれるライブラリ

次に今回使用するCSSライブラリのchakra-uiをインストールしましょう。
以下の公式サイトからパッケージインストールのコマンドをコピーして実行します。
https://chakra-ui.com/docs/getting-started

terminal
$ npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
  • このコマンドを実行することでchakra-uiが使えるようになります。
  • この記事ではchakra-uiの細かい使い方は説明しないので公式サイトで確認してください。

最後にルーティング用のパッケージをインストールします。

terminal
$ npm i react-router-dom@5.2.0
  • react-router-dom
    • ルーティング設定用のライブラリ
    • 今回はv5のreact-router-domを使いたいと思います。

3. API通信設定

ReactディレクトリでAPI通信設定をしていきましょう。
まずはsrcディレクトリの配下にapiディレクトリを作成してその配下にclient.jspost.jsファイルを作成します。そしてそこにAPI通信用の記述をしていきましょう。

ディレクトリとファイル作成

terminal
$ mkdir src/api
$ touch src/api/client.js
$ touch src/api/post.js

client.js

それではまずclient.jsにルートのエンドポイントを設定していきましょう。

src/api/client.js
import applyCaseMiddleware from "axios-case-converter";
import axios from "axios";

const options = {
  ignoreHeaders: true,
};

const client = applyCaseMiddleware(
  axios.create({
    baseURL: "http://localhost:3001/api/v1",
  }),
  options
);

export default client;
  • 細かいことは説明しませんが、とりあえずaxios-case-converterの中のapplyCaseMiddlewareを使って、Ruby側のスネークケースをJSではキャメルケースに変換するようにしています。
  • そして、ルートエンドポイントとしてhttp://localhost:3001/api/v1を設定しています。

post.js

次にpost.jsで投稿機能のAPIを設定していきます。

src/api/post.js
// 先程作成したclientをインポート
import client from "./client";

// 投稿一覧取得API
export const getAllPosts = () => {
  return client.get("/posts");
};

// 新規投稿作成API
export const createPost = (params) => {
  return client.post("/posts", params);
};
  • ここで記述した関数を用いて今後のAPI通信を作っていきます。
  • 新規投稿作成の引数であるparamsには入力するデータを入れるようにします。

4. コンポーネント作成

Home、Newコンポーネント作成

必要なコンポーネントを作成していきます。

今回は投稿一覧ページと新規投稿作成ページを作っていきます。
srcディレクトリ配下にcomponentsディレクトリを作成して、HomeコンポーネントとNewコンポーネントを作成します。

$ mkdir src/components
$ touch src/components/Home.jsx
$ touch src/components/New.jsx

Home.jsx

src/components/Home.jsx
export const Home = () => {
    return(
        <p>Homeページ</p>
    )
}

New.jsx

src/components/New.jsx
export const New = () => {
    return(
        <p>Newページ</p>
    )
}

5. ルーティング設定

次にApp.jsxで作成したコンポーネントのルーティングを設定していきます。

src/App.jsx
import React from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import { ChakraProvider, Box } from "@chakra-ui/react";

import { Home } from "./components/Home";
import { New } from "./components/New";

function App() {
  const style = {
    width: "80%",
    margin: "80px auto",
  };
  return (
    <ChakraProvider>
      <BrowserRouter>
        <Box style={style}>
          <Switch>
            <Route exact path="/" component={Home} />
            <Route exact path="/new" component={New} />
          </Switch>
        </Box>
      </BrowserRouter>
    </ChakraProvider>
  );
}

export default App;
  • 細かいスタイルの説明などはしません。
  • 今回は"/"でHomeコンポーネント、"/new"でNewコンポーネントに遷移するようにしています。
  • ChakraProviderというタグを囲まないとchakra-uiが適用されないので一番外枠に追加しています。

6. ページ作成

最後にHome、Newコンポーネントの記述をしていきます。

New.jsx

先に投稿フォームを作成してちゃんとDBにデータが追加されているか確認していきましょう。

src/components/New.jsx
import React, { useState } from "react";
import ReactTagInput from "@pathofdev/react-tag-input";
import "@pathofdev/react-tag-input/build/index.css";
import { Button, Input, Flex, Heading } from "@chakra-ui/react";
import { useHistory } from "react-router-dom";

import { createPost } from "../api/post";

export const New = () => {
  // 投稿内容ステート
  const [content, setContent] = useState("");
  
  // タグ内容ステート
  const [tags, setTags] = useState([]);
  // 配列内で重複する文字列を削除して配列を作り直す
  const deleteDoubleArray = [...new Set(tags)];
  // 配列をスペース区切りの文字列に改変
  const tagStr = deleteDoubleArray.join(" ");
  
  // Homeへページ遷移
  const history = useHistory();

  const onClickHome = () => {
    history.push("/");
  };

  // 新規投稿作成のパラメーター
  const generateParams = {
    content: content,
    tags: tagStr,
  };
  
  // フォーム関連の関数
  const handleChange = (e) => {
    setContent(e.target.value);
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      const res = await createPost(generateParams);
      console.log(res);
      setContent("");
      setTags([]);
      history.push("/");
    } catch (e) {
      console.log(e);
    }
  };
  
  return (
    <>
      <Heading fontSize="36px" mb="24px" as="h2" textAlign="center">
        新規投稿
      </Heading>
      <form>
        <Input
          mb="24px"
          type="text"
          placeholder="content"
          onChange={(e) => handleChange(e)}
          value={content}
        />
        <ReactTagInput
          placeholder="入力してください"
          tags={tags}
          onChange={(newTags) => setTags(newTags)}
          allowUnique={false}
        />
        <Flex justify="center" mt="24px">
          <Button colorScheme="teal" onClick={(e) => handleSubmit(e)}>
            投稿
          </Button>
          <Button ml="24px" onClick={onClickHome}>
            戻る
          </Button>
        </Flex>
      </form>
    </>
  );
};

一つずつ簡単に解説していきます。

useState管理

src/components/New.jsx
// 投稿内容ステート
const [content, setContent] = useState("");

// タグ内容ステート
const [tags, setTags] = useState([]);
// 配列内で重複する文字列を削除して配列を作り直す
const deleteDoubleArray = [...new Set(tags)];
// 配列をスペース区切りの文字列に改変
const tagStr = deleteDoubleArray.join(" ");
  • useStateでpostsテーブルのcontentカラムに保存する値をステート管理
  • useStateでtagsテーブルのnameカラムに保存する値をステート管理
  • react-tag-inputを使うとタグが配列で入るようになるため、tagをいれるuseStateは配列にする必要があります。
  • deleteDoubleArray変数で重複したtagを削除して重複したものもそうでないものもそれぞれ1つずつ入った配列を作り直しています。
  • 新規投稿作成用のAPIに入力するタグの値は文字列でスペース区切りにすることでレスポンスで配列に保存される作りにしているので、tagStr変数で配列に入れたtagをスペース区切りの文字列にしています。

Homeページへ遷移するための関数

src/components/New.jsx
// Homeへページ遷移
const history = useHistory();

const onClickHome = () => {
    history.push("/");
};
  • react-router-domのv5にあるuseHistoryを用いてクリックしたらHomeページに遷移するための関数を作っています。
  • この関数をJSXのタグのonClick属性に入れることで使えるようになります。

新規投稿作成のパラメーター

src/components/New.jsx
// 新規投稿作成のパラメーター
const generateParams = {
    content: content,
    tags: tagStr,
};
  • 新規投稿作成のAPI関数のパラメーターに入れるようの値を設定しています。
  • postsテーブルのcontentカラムとtagsテーブルのnameカラムに入れる文字列を設定しています。
  • tagsテーブルのnameカラムの文字列に入れるために先程配列をスペース区切りの文字列に改変したものを配置しています。

フォーム関連の関数

src/components/New.jsx
// フォーム関連の関数
const handleChange = (e) => {
    setContent(e.target.value);
};

const handleSubmit = async (e) => {
    e.preventDefault();
    try {
        const res = await createPost(generateParams);
        console.log(res);
        setContent("");
        setTags([]);
        history.push("/");
    } catch (e) {
        console.log(e);
    }
};
  • handleChange関数はpostsテーブルのcontentカラムに入れるためのテキストフィールドのonChange属性に配置することで入力した値をステート管理しています。

  • react-tag-inputを使うとvalue属性はtagsという属性に置き換わり、そのままuseStateの値を入れるだけで済むので関数を作る必要はありません。

  • handleSubmit関数ではAPI関数で作成したcreatePost関数を使い、parmasに先程作成したgenerateParamsを配置します。

  • 成功したら最後にステートの中身を空にすることでフォームの中身が空にリセットされます。

  • そして、最後に投稿一覧が表示されるHomeページに遷移されるようにしています。

JSX

次にJSXを記述します。

return (
    <>
      <Heading fontSize="36px" mb="24px" as="h2" textAlign="center">
        新規投稿
      </Heading>
      <form>
        <Input
          mb="24px"
          type="text"
          placeholder="content"
          onChange={(e) => handleChange(e)}
          value={content}
        />
        <ReactTagInput
          placeholder="入力してください"
          tags={tags}
          onChange={(newTags) => setTags(newTags)}
          allowUnique={false}
        />
        <Flex justify="center" mt="24px">
          <Button colorScheme="teal" onClick={(e) => handleSubmit(e)}>
            投稿
          </Button>
          <Button ml="24px" onClick={onClickHome}>
            戻る
          </Button>
        </Flex>
      </form>
    </>
);

一つずつ見ていきましょう。

<Input
  mb="24px"
  type="text"
  placeholder="content"
  onChange={(e) => handleChange(e)}
  value={content}
/>
  • スタイルの説明は省きます。
  • まずformタグで囲んだ中に、contentを入力するためのInputエリアとtagを入力するためのエリアを作ります。
  • contentを入力するためのInputエリアのonChange属性には先程作成したhandleChange関数を指定します。
  • valueにはcontentというステートを指定してステート管理しています。
<ReactTagInput
  placeholder="入力してください"
  tags={tags}
  onChange={(newTags) => setTags(newTags)}
/>
  • react-tag-inputを使うと利用できるReactTagInputコンポーネントを利用します。
  • このtagsという属性にはステート管理しているtagsという配列を指定します。
<Button colorScheme="teal" onClick={(e) => handleSubmit(e)}>
    投稿
</Button>
<Button ml="24px" onClick={onClickHome}>
    戻る
</Button>
  • 投稿ボタンはchakra-uiの中のButtonコンポーネントのonClick属性に、先程作成したhandleSubmit関数を指定して、API通信を行います。
  • 戻るボタンのonClick属性には先程作成したonClickHome関数を指定してHomeページへ遷移できるようにしています。

Home.jsx

最後に投稿一覧表示をするHomeコンポーネントを作成していきましょう。

src/components/Home.jsx
import { useEffect, useState } from "react";
import {
  Box,
  Center,
  Heading,
  Wrap,
  WrapItem,
  Button,
  Tag,
  Flex,
} from "@chakra-ui/react";
import { useHistory } from "react-router-dom";

import { getAllPosts } from "../api/post";

export const Home = () => {
  // 投稿一覧ステート
  const [posts, setPosts] = useState([]);

    // 投稿一覧取得API
  const handleGetAllPosts = async () => {
    try {
      const res = await getAllPosts();
      console.log(res.data);
      setPosts(res.data);
    } catch (e) {
      console.log(e);
    }
  };

  useEffect(() => {
    handleGetAllPosts();
  }, []);

  // 新規登録へページ遷移
  const history = useHistory();

  const onClickNew = () => {
    history.push("/new");
  };
  return (
    <Box>
      <Flex mb="24px" justify="center">
        <Heading fontSize="36px" mr="24px" as="h2" textAlign="center">
          Home
        </Heading>
        <Button onClick={onClickNew}>新規投稿</Button>
      </Flex>
      <Wrap mb="24px">
        {posts.map((post) => (
          <WrapItem key={post.id}>
            <Center
              width="240px"
              height="240px"
              bg="white"
              borderRadius="md"
              shadow="md"
              p="16px"
            >
              <Box width="100%">
                <Heading as="h3" fontSize="24px" textAlign="center" mb="24px">
                  {post.content}
                </Heading>
                <Wrap>
                  {post.tags.map((tag) => (
                    <WrapItem key={tag.id}>
                      <Tag bg="teal" cursor="pointer" color="white">
                        {tag.name}
                      </Tag>
                    </WrapItem>
                  ))}
                </Wrap>
              </Box>
            </Center>
          </WrapItem>
        ))}
      </Wrap>
    </Box>
  );
};

一つずつ簡単に解説していきます。

// 投稿一覧ステート
const [posts, setPosts] = useState([]);
  • 投稿一覧を取得した際にこのステートに格納します。
  • 一覧なので空の配列を入れています。
// 投稿一覧取得API
const handleGetAllPosts = async () => {
    try {
        const res = await getAllPosts();
        console.log(res.data);
        setPosts(res.data);
    } catch (e) {
        console.log(e);
    }
};

useEffect(() => {
    handleGetAllPosts();
}, []);
  • handleGetAllPosts関数でDBに保存された投稿一覧を取得しています。
  • API関数を作った際に作成したgetAllPosts関数を利用しています。
  • ここで成功したらsetPostsという更新ステートにデータを格納しています。
  • useEffectでこのhandleGetAllPostsをページを更新した際に一度だけ呼び出しています。
// 新規登録へページ遷移
const history = useHistory();

const onClickNew = () => {
    history.push("/new");
};
  • react-router-domのv5にあるuseHistoryを用いてクリックしたらNewページに遷移するための関数を作っています。
  • この関数をJSXのタグのonClick属性に入れることで使えるようになります。

JSX

次にJSXを記述します。

return (
    <Box>
      <Flex mb="24px" justify="center">
        <Heading fontSize="36px" mr="24px" as="h2" textAlign="center">
          Home
        </Heading>
        <Button onClick={onClickNew}>新規投稿</Button>
      </Flex>
      <Wrap mb="24px">
        {posts.map((post) => (
          <WrapItem key={post.id}>
            <Center
              width="240px"
              height="240px"
              bg="white"
              borderRadius="md"
              shadow="md"
              p="16px"
            >
              <Box width="100%">
                <Heading as="h3" fontSize="24px" textAlign="center" mb="24px">
                  {post.content}
                </Heading>
                <Wrap>
                  {post.tags.map((tag) => (
                    <WrapItem key={tag.id}>
                      <Tag bg="teal" cursor="pointer" color="white">
                        {tag.name}
                      </Tag>
                    </WrapItem>
                  ))}
                </Wrap>
              </Box>
            </Center>
          </WrapItem>
        ))}
      </Wrap>
    </Box>
);

一つずつ見ていきましょう。

<Button onClick={onClickNew}>新規投稿</Button>
  • 新規投稿ボタンを作成して、onClick属性に先程作成したonClickNew関数を指定しています。
  • これで、このボタンを押すとNewコンポーネントに遷移することができます。
{posts.map((post) => (
  • ここで、投稿一覧を取得したデータを格納したpostsステートをmapで回して一つずつ取り出しています。
<Box width="100%">
    <Heading as="h3" fontSize="24px" textAlign="center" mb="24px">
      {post.content}
    </Heading>
    <Wrap>
      {post.tags.map((tag) => (
        <WrapItem key={tag.id}>
          <Tag bg="teal" cursor="pointer" color="white">
	    {tag.name}
          </Tag>
        </WrapItem>
      ))}
    </Wrap>
</Box>
  • この部分はpostsをmapメソッドで回して一つずつ取り出した中身の部分になります。
  • ここで記述されている{post.content}はpostsテーブルのcontentカラムの値になります。
  • 投稿一覧を取得するとtagsという配列が中にも入っているので、それをpost.tags.mapで一つずつ回しています。
  • {tag.name}は1つの投稿の中の複数のタグを回したものを1つずつ取り出しているということになります。

これで表示を確認して初めに表示している作るものの通りに表示されたら成功です。

Discussion