Rails API + Reactで中間テーブルを用いた複数タグ投稿
やったこと
前回の「Rails APIで中間テーブルを用いた複数タグ投稿」という記事で作ったAPIを用いて実際にReactで投稿フォームから投稿してDBにデータを保存してみました。
前回の記事で作ったAPIを使っているので、上の記事を読んでから今回の記事を読んでもらえるとわかりやすいと思います。
作るもの
-
react-tag-input
というパッケージを用いて複数のタグが投稿できる投稿フォームを作りました。 - タグが重複してるものは削除してそれぞれ一つだけが保存されるようにしました。
手順
- プロジェクト作成
- 必要パッケージのインストール
- API通信設定
- コンポーネント作成
- ルーティング設定
- ページ作成
1. プロジェクト作成
任意のディレクトリに移動した後、Reactプロジェクトを作成します。
今回はcreate-react-app
で作成していきます。
$ npx create-react-app frontend
-
create-react-app
の後のプロジェクト名は任意の名前になります。今回はfrontendと指定しました。
必要パッケージのインストール
作成したReactプロジェクトに移動して、必要なパッケージをインストールしていきましょう。
まずはReactプロジェクトに移動します。
$ cd frontend
次に複数のタグを作成するためのパッケージをインストールしていきます。
$ 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通信用のパッケージをインストールしていきます。
$ npm i axios axios-case-converter
- axios
- HTTP 通信を簡単にしてくれる。
- axios-case-converter
- axios で受け取ったレスポンスの値をスネークケース → キャメルケースに変換、または送信するリクエストの値をキャメルケース → スネークケースに変換してくれるライブラリ
次に今回使用するCSSライブラリのchakra-ui
をインストールしましょう。
以下の公式サイトからパッケージインストールのコマンドをコピーして実行します。
$ npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
- このコマンドを実行することで
chakra-ui
が使えるようになります。 - この記事では
chakra-ui
の細かい使い方は説明しないので公式サイトで確認してください。
最後にルーティング用のパッケージをインストールします。
$ npm i react-router-dom@5.2.0
- react-router-dom
- ルーティング設定用のライブラリ
- 今回はv5の
react-router-dom
を使いたいと思います。
3. API通信設定
ReactディレクトリでAPI通信設定をしていきましょう。
まずはsrcディレクトリの配下にapiディレクトリを作成してその配下にclient.js
、post.js
ファイルを作成します。そしてそこにAPI通信用の記述をしていきましょう。
ディレクトリとファイル作成
$ mkdir src/api
$ touch src/api/client.js
$ touch src/api/post.js
client.js
それではまず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を設定していきます。
// 先程作成した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
export const Home = () => {
return(
<p>Homeページ</p>
)
}
New.jsx
export const New = () => {
return(
<p>Newページ</p>
)
}
5. ルーティング設定
次に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にデータが追加されているか確認していきましょう。
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管理
// 投稿内容ステート
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ページへ遷移するための関数
// Homeへページ遷移
const history = useHistory();
const onClickHome = () => {
history.push("/");
};
-
react-router-dom
のv5にあるuseHistory
を用いてクリックしたらHomeページに遷移するための関数を作っています。 - この関数をJSXのタグのonClick属性に入れることで使えるようになります。
新規投稿作成のパラメーター
// 新規投稿作成のパラメーター
const generateParams = {
content: content,
tags: tagStr,
};
- 新規投稿作成のAPI関数のパラメーターに入れるようの値を設定しています。
- postsテーブルのcontentカラムとtagsテーブルのnameカラムに入れる文字列を設定しています。
- tagsテーブルのnameカラムの文字列に入れるために先程配列をスペース区切りの文字列に改変したものを配置しています。
フォーム関連の関数
// フォーム関連の関数
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コンポーネントを作成していきましょう。
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