【5.ユーザに紐付けた投稿編】Ruby on Rails + ReactでSNSアプリを作る
こんにちは、ミニマリストいずです。
Rail +ReactでSNS機能を持ったWEBアプリの作り方を紹介していく連続企画の第5弾です。
今回は投稿をユーザに紐付けたカテゴリ・メモ投稿ができるようにしていきます。
初学者の方にもわかるようにまとめていきますが、不明点がありましたら以下から質問をいただければと思います。
作成するアプリの機能紹介(再掲)
サンプル動画(再掲)
作成する機能一覧(再掲)
- ユーザ管理機能
- 新規登録
- ログイン
- メモカテゴリ機能
- 作成
- 取得
- 動画内ではクレドと表現されている部分
- メモ機能
- 作成
- 一覧取得
- 詳細取得
- 動画内ではアクションメモと表現されている部分
- コメント機能
- 作成
- 取得
Webアプリの基本であるCRUDを実装しています。
どのアプリを作るにも参考になると思いますので、初学者の方にもおすすめのアプリになっています。
ユーザ情報を含めたカテゴリ登録機能(バックエンド編)
今回も各実行後の確認方法は省きますね。
もし確認方法を忘れてしまったら、前の記事に戻って確認してみてください。
もしそれでもわからなければ以下から無料質問してみてください。
ユーザーのモデル追加時に、アソシエーションの設定はまとめてやりましたので、今回はコントローラーから変更していきます。
ユーザ情報を渡すコントローラーの追加
api/v1/users_controller.rbとなるようにファイルを追加し、以下のように変更します。
1class Api::V1::UsersController < ApplicationController
2 before_action :authenticate_api_v1_user!
3
4 def user_name
5 if current_api_v1_user
6 render json: { name: current_api_v1_user.name}
7 else
8 render json: { error: 'No user signed in' }, status: :unauthorized
9 end
10 end
11end
ユーザ情報を含めたカテゴリ登録
以下のように、ユーザ情報を取得し、カテゴリを保存できるようにします。
※動作確認の際、ログインしていないとユーザ情報が取得できないので注意してください。
1class Api::V1::CategoriesController < ApplicationController
2 before_action :authenticate_api_v1_user!, only: [:create]
3 before_action :set_user, only: [:create]
4
5 def index
6 categories = Category.all
7 render json: categories
8 end
9
10 def create
11 category = Category.new(category_params)
12 if category.save
13 render json: category, status: :created
14 else
15 render json: category.errors, status: :unprocessable_entity
16 end
17 end
18
19 private
20
21 def category_params
22 params.require(:category).permit(:name).merge(user_id: @user.id)
23 end
24
25 def set_user
26 @user = current_api_v1_user
27 end
28end
29
ルーティングを追加
ユーザ情報が取得できるようにルーティングを追加します。
1Rails.application.routes.draw do
2 namespace :api do
3 namespace :v1 do
4 mount_devise_token_auth_for 'User', at: 'auth', controllers: {
5 registrations: 'api/v1/auth/registrations',
6 sessions: 'api/v1/auth/sessions',
7 }
8
9 resources :categories, only: [:index, :create]
10 resources :memos, only: [:create]
11 resources :users, only: [] do
12 collection do
13 get :user_name
14 end
15 end
16 end
17 end
18end
19
ユーザ情報を含めたカテゴリ登録機能(フロントエンド編)
早速、カテゴリがユーザに紐づいた状態で投稿できるようにフロントも変更していきます。
バックエンドとフロントエンドが交互に続きますので、ファイル名等をしっかり確認し、変更するようにしてください。
ユーザ情報の取得
まずはユーザ情報を取得し表示できるようにします。
1import React, { useState, useEffect } from 'react';
2import {
3 Box,
4 Input,
5 Button,
6 Center,
7
8} from "@chakra-ui/react";
9import { useToast } from "@chakra-ui/react";
10import axios from 'axios';
11
12export default function NewCategory() {
13 const [category, setCategory] = useState('');
14 const [userName, setUserName] = useState('');
15 const [isLoading, setIsLoading] = useState(false);
16 const toast = useToast();
17
18 useEffect(() => {
19 fetchGetUserName();
20 }
21 , []);
22
23 async function fetchGetUserName() {
24 try {
25 const res = await axios.get("http://localhost:3010/api/v1/users/user_name", {
26 headers: {
27 'access-token': localStorage.getItem('access-token'),
28 'client': localStorage.getItem('client'),
29 'uid': localStorage.getItem('uid'),
30 }
31 });
32
33 if (!res.status || (res.status < 200 && res.status >= 300)) {
34 throw new Error(`HTTP error! status: ${res.status}`);
35 }
36
37 setUserName(res.data.name);
38 }
39 catch (error) {
40 console.error('Error creating credos:', error);
41 toast({
42 title: 'ユーザー名の取得に失敗しました。',
43 status: 'error',
44 isClosable: true,
45 });
46 }
47 }
48
49 async function fetchCreateCategory() {
50 setIsLoading(true);
51
52 try {
53 if (!category) {
54 toast({
55 title: 'カテゴリを入力して下さい。',
56 status: 'error',
57 isClosable: true,
58 });
59
60 return;
61 }
62
63 const res = await axios.post("http://localhost:3010/api/v1/categories", {
64 category: {
65 name: category,
66 },
67 });
68
69 if (!res.status || (res.status < 200 && res.status >= 300)) {
70 throw new Error(`HTTP error! status: ${res.status}`);
71 }
72
73 toast({
74 title: 'カテゴリを登録しました。',
75 status: 'success',
76 isClosable: true,
77 });
78 }
79 catch (error) {
80 console.error('Error creating credos:', error);
81 toast({
82 title: 'カテゴリの登録に失敗しました。',
83 status: 'error',
84 isClosable: true,
85 });
86 }
87 finally {
88 setIsLoading(false);
89 setCategory('');
90 }
91 }
92
93 return (
94 <Center>
95 <Box w={["100%", "90%", "80%", "70%", "60%"]} mt={["50px", "100px", "150px", "200px"]}>
96 {"ユーザ名 : "+userName}
97 <Input
98 mt={6}
99 value={category}
100 onChange={(e) => setCategory(e.target.value)}
101 placeholder="追加したいカテゴリを入力" />
102 <Center>
103 <Button mt="6px" onClick={fetchCreateCategory} isLoading={isLoading} disabled={!userName || !category}>登録</Button>
104 </Center>
105 </Box>
106 </Center>
107 );
108}
以下のようにユーザ名が表示できていたらOKです。
ユーザ情報を含めたカテゴリ登録のリクエスト
次に、カテゴリ作成のリクエストのヘッダーにユーザ情報を含めて送り、ユーザ情報含めてカテゴリ登録をできるようにします。
1import React, { useState, useEffect } from 'react';
2import {
3 Box,
4 Input,
5 Button,
6 Center,
7
8} from "@chakra-ui/react";
9import { useToast } from "@chakra-ui/react";
10import axios from 'axios';
11
12export default function NewCategory() {
13 const [category, setCategory] = useState('');
14 const [userName, setUserName] = useState('');
15 const [isLoading, setIsLoading] = useState(false);
16 const toast = useToast();
17
18 useEffect(() => {
19 fetchGetUserName();
20 }
21 , []);
22
23 async function fetchGetUserName() {
24 try {
25 const res = await axios.get("http://localhost:3010/api/v1/users/user_name", {
26 headers: {
27 'access-token': localStorage.getItem('access-token'),
28 'client': localStorage.getItem('client'),
29 'uid': localStorage.getItem('uid'),
30 }
31 });
32
33 if (!res.status || (res.status < 200 && res.status >= 300)) {
34 throw new Error(`HTTP error! status: ${res.status}`);
35 }
36
37 setUserName(res.data.name);
38 }
39 catch (error) {
40 console.error('Error creating credos:', error);
41 toast({
42 title: 'ユーザー名の取得に失敗しました。',
43 status: 'error',
44 isClosable: true,
45 });
46 }
47 }
48
49 async function fetchCreateCategory() {
50 setIsLoading(true);
51
52 try {
53 if (!category) {
54 toast({
55 title: 'カテゴリを入力して下さい。',
56 status: 'error',
57 isClosable: true,
58 });
59
60 return;
61 }
62
63 const res = await axios.post("http://localhost:3010/api/v1/categories", {
64 category: {
65 name: category,
66 },},
67 {
68 headers: {
69 'access-token': localStorage.getItem('access-token'),
70 'client': localStorage.getItem('client'),
71 'uid': localStorage.getItem('uid'),
72 }
73 });
74
75 if (!res.status || (res.status < 200 && res.status >= 300)) {
76 throw new Error(`HTTP error! status: ${res.status}`);
77 }
78
79 toast({
80 title: 'カテゴリを登録しました。',
81 status: 'success',
82 isClosable: true,
83 });
84 }
85 catch (error) {
86 console.error('Error creating credos:', error);
87 toast({
88 title: 'カテゴリの登録に失敗しました。',
89 status: 'error',
90 isClosable: true,
91 });
92 }
93 finally {
94 setIsLoading(false);
95 setCategory('');
96 }
97 }
98
99 return (
100 <Center>
101 <Box w={["100%", "90%", "80%", "70%", "60%"]} mt={["50px", "100px", "150px", "200px"]}>
102 {"ユーザ名 : "+userName}
103 <Input
104 mt={6}
105 value={category}
106 onChange={(e) => setCategory(e.target.value)}
107 placeholder="追加したいカテゴリを入力" />
108 <Center>
109 <Button mt="6px" onClick={fetchCreateCategory} isLoading={isLoading} disabled={!userName || !category}>登録</Button>
110 </Center>
111 </Box>
112 </Center>
113 );
114}
以下のようになったらOKです。
ユーザ情報を含めたメモ登録機能(バックエンド編)
メモ登録機能はカテゴリの取得をしていますので、先に取得できるカテゴリをユーザに紐づいたものに変更していきます。
ユーザに紐づいたカテゴリの取得
1class Api::V1::CategoriesController < ApplicationController
2 before_action :authenticate_api_v1_user!, only: [:index, :create]
3 before_action :set_user, only: [:index, :create]
4
5 def index
6 categories = @user.categories
7 render json: categories
8 end
9
10 def create
11 category = Category.new(category_params)
12 if category.save
13 render json: category, status: :created
14 else
15 render json: category.errors, status: :unprocessable_entity
16 end
17 end
18
19 private
20
21 def category_params
22 params.require(:category).permit(:name).merge(user_id: @user.id)
23 end
24
25 def set_user
26 @user = current_api_v1_user
27 end
28end
29
次にユーザ情報を含めたメモを登録できるようにコントローラーを変更していきます。
ユーザ情報を含めたメモの登録
1class Api::V1::MemosController < ApplicationController
2 before_action :authenticate_api_v1_user!, only: [:create]
3 before_action :set_user, only: [:create]
4
5 def create
6 memo = Memo.new(memo_params)
7 if memo.save
8 render json: memo, status: :created
9 else
10 render json: memo.errors, status: :unprocessable_entity
11 end
12 end
13
14 private
15
16 def memo_params
17 params.require(:memo).permit(:content, :category_id).merge(user_id: @user.id)
18 end
19
20 def set_user
21 @user = current_api_v1_user
22 end
23end
24
次にフロントエンドの方を変更していきます。
ユーザ情報を含めたメモ登録機能(フロントエンド編)
先程の流れ同様に、まずはユーザ情報を取得できるようにします。
ユーザ情報とユーザ情報を含めたカテゴリの取得
1import React, { useEffect, useState } from 'react';
2import {
3 Box,
4 Input,
5 Button,
6 Center,
7 Select,
8} from "@chakra-ui/react";
9import { useToast } from "@chakra-ui/react";
10import axios from 'axios';
11
12export default function NewMemo() {
13 const [ categories, setCategories] = useState([]);
14 const [ category_id, setCategoryId ] = useState('');
15 const [ memo, setMemo ] = useState('');
16 const [ userName, setUserName ] = useState('');
17 const [ isLoading, setIsLoading ] = useState(false);
18 const toast = useToast();
19
20 useEffect(() => {
21 fetchGetUserName();
22 fetchGetCategory();
23 }, [])
24
25 async function fetchGetUserName() {
26 try {
27 const res = await axios.get("http://localhost:3010/api/v1/users/user_name", {
28 headers: {
29 'access-token': localStorage.getItem('access-token'),
30 'client': localStorage.getItem('client'),
31 'uid': localStorage.getItem('uid'),
32 }
33 });
34
35 if (!res.status || (res.status < 200 && res.status >= 300)) {
36 throw new Error(`HTTP error! status: ${res.status}`);
37 }
38
39 setUserName(res.data.name);
40 }
41 catch (error) {
42 console.error('Error creating credos:', error);
43 toast({
44 title: 'ユーザー名の取得に失敗しました。',
45 status: 'error',
46 isClosable: true,
47 });
48 }
49 }
50
51 async function fetchGetCategory() {
52 try {
53 const res = await axios.get("http://localhost:3010/api/v1/categories", {
54 headers: {
55 'access-token': localStorage.getItem('access-token'),
56 'client': localStorage.getItem('client'),
57 'uid': localStorage.getItem('uid'),
58 }
59 });
60
61 if (!res.status || (res.status < 200 && res.status >= 300)) {
62 throw new Error(`HTTP error! status: ${res.status}`);
63 }
64
65 setCategories(res.data);
66 }
67 catch (error) {
68 console.error('Error creating credos:', error);
69 toast({
70 title: 'カテゴリの取得に失敗しました。',
71 status: 'error',
72 isClosable: true,
73 });
74 }
75 }
76
77 async function fetchCreateMemo() {
78 setIsLoading(true);
79
80 try {
81 if (!category_id || !memo) {
82 toast({
83 title: 'カテゴリの選択とメモの入力をして下さい。',
84 status: 'error',
85 isClosable: true,
86 });
87
88 return;
89 }
90
91 const res = await axios.post("http://localhost:3010/api/v1/memos", {
92 memo: {
93 content: memo,
94 category_id
95 }});
96
97 if (!res.status || (res.status < 200 && res.status >= 300)) {
98 throw new Error(`HTTP error! status: ${res.status}`);
99 }
100
101 toast({
102 title: 'メモを登録しました。',
103 status: 'success',
104 isClosable: true,
105 });
106 }
107 catch (error) {
108 console.error('Error creating credos:', error);
109 toast({
110 title: 'メモの登録に失敗しました。',
111 status: 'error',
112 isClosable: true,
113 });
114 }
115 finally {
116 setIsLoading(false);
117 setMemo('');
118 }
119 }
120
121 return (
122 <Center>
123 <Box w={["100%", "90%", "80%", "70%", "60%"]} mt={["50px", "100px", "150px", "200px"]}>
124 {"ユーザ名 : "+userName}
125 <Select
126 mt={6}
127 onChange={(e) => setCategoryId(e.target.value)}
128 placeholder='カテゴリを選択して下さい。'
129 >
130 {categories.length > 0 && categories.map((item, index) => {
131 return <option value={item.id} key={index}>{item.name}</option>;
132 })}
133 </Select>
134 <Input
135 value={memo}
136 onChange={(e) => setMemo(e.target.value)}
137 placeholder="追加したいメモを入力"
138 />
139 <Center>
140 <Button mt="6px" onClick={fetchCreateMemo} isLoading={isLoading} disabled={!memo || !category_id}>送信</Button>
141 </Center>
142 </Box>
143 </Center>
144 );
145 }
以下のようになっていればOKです。
ユーザ情報を含めたメモの登録
次にユーザ情報をリクエストヘッダに含めて、メモの登録をできるようにします。
1import React, { useEffect, useState } from 'react';
2import {
3 Box,
4 Input,
5 Button,
6 Center,
7 Select,
8} from "@chakra-ui/react";
9import { useToast } from "@chakra-ui/react";
10import axios from 'axios';
11
12export default function NewMemo() {
13 const [ categories, setCategories] = useState([]);
14 const [ category_id, setCategoryId ] = useState('');
15 const [ memo, setMemo ] = useState('');
16 const [ userName, setUserName ] = useState('');
17 const [ isLoading, setIsLoading ] = useState(false);
18 const toast = useToast();
19
20 useEffect(() => {
21 fetchGetUserName();
22 fetchGetCategory();
23 }, [])
24
25 async function fetchGetUserName() {
26 try {
27 const res = await axios.get("http://localhost:3010/api/v1/users/user_name", {
28 headers: {
29 'access-token': localStorage.getItem('access-token'),
30 'client': localStorage.getItem('client'),
31 'uid': localStorage.getItem('uid'),
32 }
33 });
34
35 if (!res.status || (res.status < 200 && res.status >= 300)) {
36 throw new Error(`HTTP error! status: ${res.status}`);
37 }
38
39 setUserName(res.data.name);
40 }
41 catch (error) {
42 console.error('Error creating credos:', error);
43 toast({
44 title: 'ユーザー名の取得に失敗しました。',
45 status: 'error',
46 isClosable: true,
47 });
48 }
49 }
50
51 async function fetchGetCategory() {
52 try {
53 const res = await axios.get("http://localhost:3010/api/v1/categories", {
54 headers: {
55 'access-token': localStorage.getItem('access-token'),
56 'client': localStorage.getItem('client'),
57 'uid': localStorage.getItem('uid'),
58 }
59 });
60
61 if (!res.status || (res.status < 200 && res.status >= 300)) {
62 throw new Error(`HTTP error! status: ${res.status}`);
63 }
64
65 setCategories(res.data);
66 }
67 catch (error) {
68 console.error('Error creating credos:', error);
69 toast({
70 title: 'カテゴリの取得に失敗しました。',
71 status: 'error',
72 isClosable: true,
73 });
74 }
75 }
76
77 async function fetchCreateMemo() {
78 setIsLoading(true);
79
80 try {
81 if (!category_id || !memo) {
82 toast({
83 title: 'カテゴリの選択とメモの入力をして下さい。',
84 status: 'error',
85 isClosable: true,
86 });
87
88 return;
89 }
90
91 const res = await axios.post("http://localhost:3010/api/v1/memos", {
92 memo: {
93 content: memo,
94 category_id
95 }},
96 {
97 headers: {
98 'access-token': localStorage.getItem('access-token'),
99 'client': localStorage.getItem('client'),
100 'uid': localStorage.getItem('uid'),
101 }
102 });
103
104 if (!res.status || (res.status < 200 && res.status >= 300)) {
105 throw new Error(`HTTP error! status: ${res.status}`);
106 }
107
108 toast({
109 title: 'メモを登録しました。',
110 status: 'success',
111 isClosable: true,
112 });
113 }
114 catch (error) {
115 console.error('Error creating credos:', error);
116 toast({
117 title: 'メモの登録に失敗しました。',
118 status: 'error',
119 isClosable: true,
120 });
121 }
122 finally {
123 setIsLoading(false);
124 setMemo('');
125 }
126 }
127
128 return (
129 <Center>
130 <Box w={["100%", "90%", "80%", "70%", "60%"]} mt={["50px", "100px", "150px", "200px"]}>
131 {"ユーザ名 : "+userName}
132 <Select
133 mt={6}
134 onChange={(e) => setCategoryId(e.target.value)}
135 placeholder='カテゴリを選択して下さい。'
136 >
137 {categories.length > 0 && categories.map((item, index) => {
138 return <option value={item.id} key={index}>{item.name}</option>;
139 })}
140 </Select>
141 <Input
142 value={memo}
143 onChange={(e) => setMemo(e.target.value)}
144 placeholder="追加したいメモを入力"
145 />
146 <Center>
147 <Button mt="6px" onClick={fetchCreateMemo} isLoading={isLoading} disabled={!memo || !category_id}>送信</Button>
148 </Center>
149 </Box>
150 </Center>
151 );
152 }
ただコードを脳死で写しているだけになっているなと思ったら、以下から無料質問もしてみてください。
学習方法を含めて相談に乗らせていただきます。
以下のようになっていたらOKです。
まとめ
SNSアプリを作成していくにあたり、RailsとReactを連携し、ユーザに紐づいたカテゴリ、メモの作成ができるようになりました。
次回はユーザに紐づいたメモの取得をできるようにしていきます。