create

【5.ユーザに紐付けた投稿編】Ruby on Rails + ReactでSNSアプリを作る

pepe87
記事内に商品プロモーションを含む場合があります

こんにちは、ミニマリストいずです。

Rail +ReactでSNS機能を持ったWEBアプリの作り方を紹介していく連続企画の第5弾です。

今回は投稿をユーザに紐付けたカテゴリ・メモ投稿ができるようにしていきます。

初学者の方にもわかるようにまとめていきますが、不明点がありましたら以下から質問をいただければと思います。

作成するアプリの機能紹介(再掲)

サンプル動画(再掲)

作成する機能一覧(再掲)

作成する機能一覧
  • ユーザ管理機能
    • 新規登録
    • ログイン
  • メモカテゴリ機能
    • 作成
    • 取得
    • 動画内ではクレドと表現されている部分
  • メモ機能
    • 作成
    • 一覧取得
    • 詳細取得
    • 動画内ではアクションメモと表現されている部分
  • コメント機能
    • 作成
    • 取得

Webアプリの基本であるCRUDを実装しています。

どのアプリを作るにも参考になると思いますので、初学者の方にもおすすめのアプリになっています。

ユーザ情報を含めたカテゴリ登録機能(バックエンド編)

今回も各実行後の確認方法は省きますね。

もし確認方法を忘れてしまったら、前の記事に戻って確認してみてください。

あわせて読みたい
【1.カテゴリ新規登録編】Ruby on Rails + ReactでSNSアプリを作る
【1.カテゴリ新規登録編】Ruby on Rails + ReactでSNSアプリを作る

もしそれでもわからなければ以下から無料質問してみてください。

ユーザーのモデル追加時に、アソシエーションの設定はまとめてやりましたので、今回はコントローラーから変更していきます。

ユーザ情報を渡すコントローラーの追加

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

authenticate_api_v1_user!true(つまり、ユーザーが認証されている)で、current_api_v1_usernil(つまり、現在のユーザーが存在しない)になることは、通常ありません。

authenticate_api_v1_user!メソッドは、ユーザーが認証されていない場合にエラーレスポンスを返し、アクションの実行を停止します。したがって、このメソッドがtrueを返した場合、current_api_v1_userは認証されたユーザーを返すはずです。

ただし、何らかの理由で認証情報が破損しているか、またはユーザーが認証後に削除されたなどの例外的な状況では、このような状況が発生する可能性があります。そのような場合、アプリケーションのエラーハンドリングが適切に行われるべきです。

ユーザ情報を含めたカテゴリ登録

以下のように、ユーザ情報を取得し、カテゴリを保存できるようにします。

※動作確認の際、ログインしていないとユーザ情報が取得できないので注意してください。

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

collection do ブロックは、リソースの特定のインスタンスではなく、リソース全体に対するルートを定義します。この場合、get :user_nameは、HTTP GETリクエストをuser_nameアクションにルーティングします。

具体的には、/your_resource/user_nameというURLが、対応するコントローラのuser_nameアクションにルーティングされます。ここでyour_resourceは、このコードが含まれるresourcesブロックの名前に置き換えられます。

ユーザ情報を含めたカテゴリ登録機能(フロントエンド編)

早速、カテゴリがユーザに紐づいた状態で投稿できるようにフロントも変更していきます。

バックエンドとフロントエンドが交互に続きますので、ファイル名等をしっかり確認し、変更するようにしてください。

ユーザ情報の取得

まずはユーザ情報を取得し表示できるようにします。

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}

‘access-token’: localStorage.getItem(‘access-token’)等の認証トークンをリクエストのヘッダにふくめているため、コントローラー側でユーザ情報を取得し、カテゴリに紐づけることができています。

以下のようになったら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を連携し、ユーザに紐づいたカテゴリ、メモの作成ができるようになりました。

次回はユーザに紐づいたメモの取得をできるようにしていきます。

ABOUT ME
いず
いず
ライフコーチ・ミニマリスト・IT系
Hard Funな時間を増やすためにミニマリストに。ライフコーチ、プログラミングのサービス開発・メンターをやりながら、ブログを運営。ミニマリストのおすすめアイテムや、考え方を発信していきます。
Recommend
こちらの記事もどうぞ
記事URLをコピーしました