~part4~【FC版】React + Rails API + axios + react-router-domでCRUDを実装する

こんにちは！スージです。
[こちら](https://qiita.com/suzy1031/items/ab1186dc3ae48be27e5a)の記事の続きです

### やりたい事

- `Userモデル`と`Postモデル`を1対多で関連付け
- ユーザーが登録した投稿を一覧画面に表示
- 新規登録した時に`user_id`を保存
- ログイン中のユーザーは自分が投稿したデータのみ`更新`・`削除`ができる

### 開発環境
Ruby 2.7.2
Rails 6.1.4
MySQL
node.js 16.6.2
React 17.0.2

### 参考
[Rails deviseで使えるようになるヘルパーメソッド一覧](https://qiita.com/tobita0000/items/866de191635e6d74e392)

### Rails側から実装開始

まず、postテーブルに外部キー`user_id`を追加します

```terminal
backend $ rails g migration AddColumnsToPosts
```

```ruby
# 日付_add_columns_to_posts.rb
class AddColumnsToPosts < ActiveRecord::Migration[6.1]
  def change
    add_reference :posts, :user, foreign_key: true, after: :neko_type
  end
end
```

```terminal
rails db:migrate
```

`user_id`カラムが追加されました

```ruby
schema.rb
  create_table "posts", charset: "utf8mb4", force: :cascade do |t|
    t.string "name"
    t.string "neko_type"
    t.bigint "user_id" # 追加した外部キー
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["user_id"], name: "index_posts_on_user_id"
  end
```

### アソシエーション

```ruby
# user.rb
class User < ActiveRecord::Base
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  include DeviseTokenAuth::Concerns::User

  has_many :posts, dependent: :destroy #追加
end
```

```ruby
# post.rb
class Post < ApplicationRecord
  belongs_to :user # 追加
  has_one :detail_info, dependent: :destroy
end
```

### ルーティング

```ruby
# routes.rb
Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :posts
      resources :users # 追加

      mount_devise_token_auth_for 'User', at: 'auth', controllers: {
        registrations: 'api/v1/auth/registrations'
      }

      namespace :auth do
        resources :sessions, only: %i[index]
      end
    end
  end
end
```

### コントローラ作成

```terminal
rails g controller api/v1/users
```

showアクションに処理を書きます。ログイン中のユーザーが投稿したpostと紐づくdetail_infoを取得できるようにしておきます

```ruby
#users_controller.rb
class Api::V1::UsersController < ApplicationController
  def show
    render json: Post.where(user_id: params[:id]).to_json(include: :detail_info)
  end

end
```

### ダミーデータ修正

最終的に画面で確認した時に分かりやすいようにユーザーを2人登録します。

```ruby
# seeds.rb
User.create!(name: 'test1', email: 'test@example.com', password: 'password', password_confirmation: 'password')
User.create!(name: 'test2', email: 'test2@example.com', password: 'password', password_confirmation: 'password')

user1 = User.find(1)
post1 = Post.create!(name: 'ニャア', neko_type: 'アメリカンショートヘア', user: user1)
DetailInfo.create!(post: post1, favorite_food: '魚', favorite_toy: '猫じゃらし')

post2 = Post.create!(name: 'まる', neko_type: 'スコッティシュフォールド', user: user1)
DetailInfo.create!(post: post2, favorite_food: '野菜', favorite_toy: 'まりたん')

user2 = User.find(2)
post3 = Post.create!(name: 'むぎ', neko_type: 'スコッティシュフォールド', user: user2)
DetailInfo.create!(post: post3, favorite_food: '肉', favorite_toy: 'ダンボール')

post4 = Post.create!(name: 'ばに', neko_type: 'エキゾチックショートヘア', user: user2)
DetailInfo.create!(post: post4, favorite_food: '豆', favorite_toy: 'キャットタワー')
```

ダミーデータを入れ直します

```terminal
rails db:migrate:reset
rails db:seed
```

curlコマンドでデータを取得できる事を確認します

```terminal
// user_id: 1
curl http://localhost:5000/api/v1/users/1
[{"id":1,"name":"ニャア","neko_type":"アメリカンショートヘア","user_id":1,"created_at":"2021-08-14T13:48:21.707Z","updated_at":"2021-08-14T13:48:21.707Z",
"detail_info":{"id":1,"post_id":1,"favorite_food":"魚","favorite_toy":"猫じゃらし","created_at":"2021-08-14T13:48:21.717Z","updated_at":"2021-08-14T13:48:21.717Z"}},
{"id":2,"name":"まる","neko_type":"スコッティシュフォールド","user_id":1,"created_at":"2021-08-14T13:48:21.719Z","updated_at":"2021-08-14T13:48:21.719Z",
"detail_info":{"id":2,"post_id":2,"favorite_food":"野菜","favorite_toy":"まりたん","created_at":"2021-08-14T13:48:21.720Z","updated_at":"2021-08-14T13:48:21.720Z"}}]

// user_id: 2
[{"id":3,"name":"むぎ","neko_type":"スコッティシュフォールド","user_id":2,"created_at":"2021-08-14T13:48:21.722Z","updated_at":"2021-08-14T13:48:21.722Z",
"detail_info":{"id":3,"post_id":3,"favorite_food":"肉","favorite_toy":"ダンボール","created_at":"2021-08-14T13:48:21.723Z","updated_at":"2021-08-14T13:48:21.723Z"}},
{"id":4,"name":"ばに","neko_type":"エキゾチックショートヘア","user_id":2,"created_at":"2021-08-14T13:48:21.725Z","updated_at":"2021-08-14T13:48:21.725Z",
"detail_info":{"id":4,"post_id":4,"favorite_food":"豆","favorite_toy":"キャットタワー","created_at":"2021-08-14T13:48:21.731Z","updated_at":"2021-08-14T13:48:21.731Z"}}]
```

作成したユーザー2人に紐づく`posts`と、postに紐づく`detail_info`がそれぞれ2件づつ取得できています

### postsコントローラの修正

フロントで制御しますが、自分の投稿以外を`更新`、`削除`できないように処理を修正します

```ruby
# application_controller.rb
class ApplicationController < ActionController::Base
        include DeviseTokenAuth::Concerns::SetUserByToken

        skip_before_action :verify_authenticity_token
        helper_method :current_user, :user_signed_in?, :authenticate_user!
end
```

`devise`のヘルパーメソッド`authenticate_user!`を追加します

```ruby
# posts_controller.rb
class Api::V1::PostsController < ApplicationController
  before_action :authenticate_api_v1_user!, only: [:create, :update, :destroy] # 追加

  def index
    # 省略
  end

  def show
    # 省略
  end

  def create
    # 省略
  end

  def update
    post = Post.find(params[:id])
    detail_info = DetailInfo.find_by(post_id: params[:id])

    if current_api_v1_user.id == post.user_id # 追加
      ActiveRecord::Base.transaction do
          if post.update(post_params) && detail_info.update(detail_info_params)
            render json: post.to_json(include: :detail_info)
          else
            render json: { post: post.errors, detail_info: detail_info.errors }, status: 422
          end
      end
    else
      render json: { message: 'can not update data' }, status: 422
    end
  end

  def destroy
    post = Post.find(params[:id])
    if current_api_v1_user.id == post.user_id # 追加
      post.destroy
      render json: post.to_json(include: :detail_info)
    else
      render json: { message: 'can not delete data' }, status: 422
    end
  end

  private

    def post_params
      params.require(:post).permit(:name, :neko_type).merge(user_id: current_api_v1_user.id) # merge部分を追加
    end

    def detail_info_params
      # 省略
    end
end
```

- `before_action`を追加
- `update`アクション内に`current_user.id`と`post.user_id`が一致した場合に更新する条件を追加
- `destroy`アクション内に`current_user.id`と`post.user_id`が一致した場合に更新する条件を追加
- 新規作成する時にuser_idを一緒に保存できるように、ストロングパラメータに`.merge(user_id: current_api_v1_user.id)`を追加

### 次にReact側を実装

Rails側で追加したエンドポイントにリクエストを投げるクライアントを作成します

```terminal
frontend $ touch /src/lib/api/user.js
```

```js
// /src/lib/api/user.js
import client from './client';
import Cookies from 'js-cookie';

export const getUserPosts = (id) => {
  return client.get(`/users/${id}`, {
    headers: {
      'access-token': Cookies.get('_access_token'),
      client: Cookies.get('_client'),
      uid: Cookies.get('_uid'),
    },
  });
};
```

### 画面を用意する

その前に、ひと手間あります。一覧画面(List.jsx)と同じテーブル構造のレイアウトにするので`List.jsx`で記述したレイアウトをコンポーネントに切り出して、共通で使用します

```terminal
touch /src/components/commons/ListTable.jsx
```

```jsx
// /src/components/commons/ListTable.jsx
import React from 'react';
import { Link } from 'react-router-dom';
// style
import {
  Button,
  TableContainer,
  Table,
  TableBody,
  TableCell,
  TableRow,
  TableHead,
  Paper,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
// functions
import { subString } from '../../functions/functions';

const useStyles = makeStyles({
  table: {
    minWidth: 650,
  },
  fontWeight: {
    fontWeight: 900,
  },
});

const ListTable = (props) => {
  const classes = useStyles();
  const { dataList, handleDelete, currentUser } = props;

  return (
    <TableContainer component={Paper}>
      <Table className={classes.table} aria-label='simple table'>
        <TableHead>
          <TableRow>
            <TableCell align='center' className={classes.fontWeight}>
              名前
            </TableCell>
            <TableCell align='center' className={classes.fontWeight}>
              猫種
            </TableCell>
            <TableCell align='center' className={classes.fontWeight}>
              好きな食べ物
            </TableCell>
            <TableCell align='center' className={classes.fontWeight}>
              好きなおもちゃ
            </TableCell>
            <TableCell align='center'></TableCell>
            <TableCell align='center'></TableCell>
            <TableCell align='center'></TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {dataList.map((item, index) => (
            <TableRow key={index}>
              <TableCell align='center'>{subString(item.name, 15)}</TableCell>
              <TableCell align='center'>
                {subString(item.nekoType, 15)}
              </TableCell>
              <TableCell align='center'>
                {subString(item.detailInfo.favoriteFood, 10)}
              </TableCell>
              <TableCell align='center'>
                {subString(item.detailInfo.favoriteToy, 10)}
              </TableCell>
              {currentUser.id === item.userId ? (
                <TableCell align='center'>
                  <Link to={`/edit/${item.id}`}>更新</Link>
                </TableCell>
              ) : (
                <TableCell align='center'></TableCell>
              )}
              <TableCell align='center'>
                <Link to={`/post/${item.id}`}>詳細へ</Link>
              </TableCell>
              {currentUser.id === item.userId ? (
                <TableCell align='center'>
                  <Button
                    variant='contained'
                    color='secondary'
                    onClick={() => handleDelete(item)}
                  >
                    削除
                  </Button>
                </TableCell>
              ) : (
                <TableCell align='center'></TableCell>
              )}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
};
export default ListTable;
```


- 親コンポーネントからは`dataList`, `handleDelete`, `currentUser`を受け取ります
- `currentUser.id === item.userId ? 処理A : 処理B`で分岐して、自分の投稿以外は`編集`・`削除`のリンク・ボタンがそれぞれ表示されないよう制御します

### List.jsxを修正

テーブル部分を子コンポーネントに切り出したので、修正します

```jsx
// /src/components/List.jsx
import React, { useEffect, useState, useContext } from 'react';
import { getList, deletePost } from '../lib/api/post';
import { useHistory } from 'react-router-dom';
// style
import { Button } from '@material-ui/core';
// component
import ListTable from './commons/ListTable';
import SpaceRow from './commons/SpaceRow';
// context
import { AuthContext } from '../App';

const List = () => {
  const { loading, isSignedIn, setIsSignedIn, currentUser } =
    useContext(AuthContext);
  const [dataList, setDataList] = useState([]);

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

  const handleGetList = async () => {
    try {
      const res = await getList();
      console.log(res.data);
      setDataList(res.data);
    } catch (e) {
      console.log(e);
    }
  };

  const history = useHistory();

  const handleDelete = async (item) => {
    console.log('click', item.id);
    try {
      const res = await deletePost(item.id);
      console.log(res.data);
      handleGetList();
    } catch (e) {
      console.log(e.response);
    }
  };

  return (
    <>
      <h1>HOME</h1>
      <Button
        variant='contained'
        color='primary'
        onClick={() => history.push('/new')}
      >
        新規作成
      </Button>
      <SpaceRow height={20} />
      <ListTable
        dataList={dataList}
        handleDelete={handleDelete}
        currentUser={currentUser}
      />
    </>
  );
};
export default List;
```

### UserPost.jsxを作成

```terminal
touch /src/components/users/UserPost.jsx
```

```jsx
// /src/components/users/UserPost.jsx
import React, { useContext, useState, useEffect } from 'react';
import { Redirect, useHistory } from 'react-router-dom';
// style
import { Button } from '@material-ui/core';
// api
import { getUserPosts } from '../../lib/api/user';
import { deletePost } from '../../lib/api/post';
// context
import { AuthContext } from '../../App';
// component
import SpaceRow from '../commons/SpaceRow';
import ListTable from '../commons/ListTable';

const UserPost = () => {
  const { loading, isSignedIn, currentUser } = useContext(AuthContext);
  const [userPosts, setUserPosts] = useState({});
  const history = useHistory();

  useEffect(() => {
    handleGetUserPosts();
  }, [currentUser]);

  const handleGetUserPosts = async () => {
    if (!loading) {
      if (isSignedIn) {
        const res = await getUserPosts(currentUser.id);
        console.log(res.data);
        setUserPosts(res.data);
      } else {
        <Redirect to='/signin' />;
      }
    }
  };

  const handleDelete = async (item) => {
    console.log('click', item.id);
    try {
      const res = await deletePost(item.id);
      console.log(res.data);
      handleGetUserPosts();
    } catch (e) {
      console.log(e);
    }
  };
  const UserTable = () => {
    if (userPosts.length >= 1) {
      return (
        <ListTable
          dataList={userPosts}
          handleDelete={handleDelete}
          currentUser={currentUser}
        />
      );
    } else {
      return <h2>投稿はありません。</h2>;
    }
  };

  return (
    <>
      <h1>{currentUser.name}の投稿一覧</h1>
      <Button
        variant='contained'
        color='primary'
        onClick={() => history.push('/')}
      >
        戻る
      </Button>
      <SpaceRow height={20} />
      <UserTable />
    </>
  );
};
export default UserPost;
```

### ルーターに追加

```jsx
// src/App.jsx
import React, { useState, useEffect, createContext } from 'react';
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Redirect,
} from 'react-router-dom';
import Cookies from 'js-cookie';
// component
import List from './components/List';
import New from './components/New';
import Detail from './components/Detail';
import Edit from './components/Edit';
import Header from './components/commons/Header';
import SignUp from './components/users/SignUp';
import SignIn from './components/users/SignIn';
import MainContainer from './components/layout/MainContainer';
import UserPost from './components/users/UserPost';
// style
import { CssBaseline } from '@material-ui/core';
import { StylesProvider, ThemeProvider } from '@material-ui/styles';
import { theme } from './styles/theme';

import { getCurrentUser } from './lib/api/auth';

export const AuthContext = createContext();

const App = () => {
  const [loading, setLoading] = useState(true);
  const [isSignedIn, setIsSignedIn] = useState(false);
  const [currentUser, setCurrentUser] = useState({});

  const handleGetCurrentUser = async () => {
    try {
      const res = await getCurrentUser();

      if (res?.data.isLogin === true) {
        setIsSignedIn(true);
        setCurrentUser(res?.data.data);
      } else {
        console.log('no current user');
        // token有効期限が切れている場合、古いcookieを全て削除
        Cookies.remove('_access_token');
        Cookies.remove('_client');
        Cookies.remove('_uid');
      }
    } catch (e) {
      console.log(e);
    }

    setLoading(false);
  };

  useEffect(() => {
    handleGetCurrentUser();
  }, [setCurrentUser]);

  const Private = ({ children }) => {
    if (!loading) {
      if (isSignedIn) {
        return children;
      } else {
        return <Redirect to='/signin' />;
      }
    } else {
      return <></>;
    }
  };

  return (
    <>
      <StylesProvider injectFirst>
        <ThemeProvider theme={theme}>
          <AuthContext.Provider
            value={{
              loading,
              setLoading,
              isSignedIn,
              setIsSignedIn,
              currentUser,
              setCurrentUser,
            }}
          >
            <CssBaseline />

            <Router>
              <Header />
              <MainContainer>
                <Switch>
                  <Route exact path='/signup' component={SignUp} />
                  <Route exact path='/signin' component={SignIn} />
                  <Private>
                    <Route exact path='/' component={List} />
                    <Route path='/post/:id' component={Detail} />
                    <Route exact path='/new' component={New} />
                    <Route path='/edit/:id' component={Edit} />
                    {/* 追加 */}
                    <Route exact path='/user/posts' component={UserPost} />
                  </Private>
                </Switch>
              </MainContainer>
            </Router>
          </AuthContext.Provider>
        </ThemeProvider>
      </StylesProvider>
    </>
  );
};
export default App;
```

### ユーザー一覧画面へのリンク

ドロワー内のリンクを修正します

```jsx
// /src/components/Header.jsx
// 38行目あたり
const drawerItem = [
  { label: '一覧へ戻る', path: '/' },
  { label: '新規作成', path: '/new' },
  { label: '自分の投稿', path: '/user/posts' }, // 修正
];
```

### 動作確認

ログイン中のユーザーに紐づくデータだけ取れていればOK
ユーザー一覧画面（UserPost.jsx）にて、`user_id`:`1`では2件のデータが取得できています
![スクリーンショット 2021-08-16 10.53.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/5872ffd5-a764-8ac9-b6d2-d998d439c9cc.png)

一覧画面（List.jsx）では全件（4件）データが取得できています
![スクリーンショット 2021-08-16 10.54.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/c0471e03-e734-50b6-36d8-7604a878d789.png)

レイアウトが正しく表示されていればOK
`/`（Home）
![スクリーンショット 2021-08-16 11.01.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/cdaa03a6-ace6-192f-3fcb-725c9e4abb4b.png)

`/user/post`(ユーザー一覧)
![スクリーンショット 2021-08-16 11.02.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/428016/1749938b-48a6-00fd-b553-d4abbb393c63.png)



### apiクライアントに`header`を追加

[こちらの記事](https://qiita.com/suzy1031/items/cc708fc705158b774fd6#api-client%E3%82%92%E4%BD%9C%E6%88%90)で実装したapiクライアントを修正します。
具体的には、Rails側のdeviseのヘルパーメソッド`authenticate_user!`・`current_user`を使ってユーザー認証をするので、`header`にユーザー情報を載せてリクエストを投げる必要があります。

```js
// /src/lib/api/post.js
import client from './client';
import Cookies from 'js-cookie';

// 一覧
export const getList = () => {
  return client.get('/posts');
};

// 詳細
export const getDetail = (id) => {
  return client.get(`/posts/${id}`);
};

// 新規作成
// header情報を追加
export const createPost = (params) => {
  return client.post('/posts', params, {
    headers: {
      'access-token': Cookies.get('_access_token'),
      client: Cookies.get('_client'),
      uid: Cookies.get('_uid'),
    },
  });
};

// 更新
// header情報を追加
export const updatePost = (id, params) => {
  return client.patch(`/posts/${id}`, params, {
    headers: {
      'access-token': Cookies.get('_access_token'),
      client: Cookies.get('_client'),
      uid: Cookies.get('_uid'),
    },
  });
};

// 削除
// header情報を追加
export const deletePost = (id) => {
  return client.delete(`/posts/${id}`, {
    headers: {
      'access-token': Cookies.get('_access_token'),
      client: Cookies.get('_client'),
      uid: Cookies.get('_uid'),
    },
  });
};
```

`create`・`update`・`delete`のリクエストを送るapiクライアントにヘッダー情報を追加しました

ログインユーザーで`新規登録`・`更新`・`削除`ができればOK

***create***

```terminal
11:17:45 web.1  | Started POST "/api/v1/posts" for ::1 at 2021-08-16 11:17:45 +0900
11:17:45 web.1  | Processing by Api::V1::PostsController#create as HTML
11:17:45 web.1  |   Parameters: {"name"=>"test", "neko_type"=>"test", "detail_info"=>{"favorite_food"=>"test", "favorite_toy"=>"test"}, "post"=>{"name"=>"test", "neko_type"=>"test"}}
11:17:45 web.1  |   TRANSACTION (0.2ms)  BEGIN
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:54:in `post_params'
11:17:45 web.1  |   User Load (0.5ms)  SELECT `users`.* FROM `users` WHERE `users`.`uid` = 'test@example.com' LIMIT 1
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:54:in `post_params'
11:17:45 web.1  |   User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:16:in `block in create'
11:17:45 web.1  |   Post Create (0.3ms)  INSERT INTO `posts` (`name`, `neko_type`, `user_id`, `created_at`, `updated_at`) VALUES ('test', 'test', 1, '2021-08-16 02:17:45.431151', '2021-08-16 02:17:45.431151')
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:16:in `block in create'
11:17:45 web.1  |   DetailInfo Create (0.2ms)  INSERT INTO `detail_infos` (`post_id`, `favorite_food`, `favorite_toy`, `created_at`, `updated_at`) VALUES (5, 'test', 'test', '2021-08-16 02:17:45.432983', '2021-08-16 02:17:45.432983')
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:16:in `block in create'
11:17:45 web.1  |   TRANSACTION (1.1ms)  COMMIT
11:17:45 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:13:in `create'
11:17:45 web.1  |   User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
11:17:45 web.1  | Completed 200 OK in 13ms (Views: 0.1ms | ActiveRecord: 2.9ms | Allocations: 5071)
```

***update***

```terminal
11:18:58 web.1  | Started PATCH "/api/v1/posts/1" for ::1 at 2021-08-16 11:18:58 +0900
11:18:58 web.1  | Processing by Api::V1::PostsController#update as HTML
11:18:58 web.1  |   Parameters: {"name"=>"ニャアupdate", "neko_type"=>"アメリカンショートヘアupdate", "detail_info"=>{"favorite_food"=>"魚update", "favorite_toy"=>"猫じゃらしupdate"}, "id"=>"1", "post"=>{"name"=>"ニャアupdate", "neko_type"=>"アメリカンショートヘアupdate"}}
11:18:58 web.1  |   User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`uid` = 'test@example.com' LIMIT 1
11:18:58 web.1  |   Post Load (0.2ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:25:in `update'
11:18:58 web.1  |   DetailInfo Load (0.2ms)  SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 1 LIMIT 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:26:in `update'
11:18:58 web.1  |   TRANSACTION (0.1ms)  BEGIN
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:30:in `block in update'
11:18:58 web.1  |   User Load (0.2ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:30:in `block in update'
11:18:58 web.1  |   Post Update (0.5ms)  UPDATE `posts` SET `posts`.`name` = 'ニャアupdate', `posts`.`neko_type` = 'アメリカンショートヘアupdate', `posts`.`updated_at` = '2021-08-16 02:18:58.901940' WHERE `posts`.`id` = 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:30:in `block in update'
11:18:58 web.1  |   Post Load (0.3ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 1 LIMIT 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:30:in `block in update'
11:18:58 web.1  |   DetailInfo Update (0.4ms)  UPDATE `detail_infos` SET `detail_infos`.`favorite_food` = '魚update', `detail_infos`.`favorite_toy` = '猫じゃらしupdate', `detail_infos`.`updated_at` = '2021-08-16 02:18:58.908105' WHERE `detail_infos`.`id` = 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:30:in `block in update'
11:18:58 web.1  |   DetailInfo Load (0.5ms)  SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 1 LIMIT 1
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:31:in `block in update'
11:18:58 web.1  |   TRANSACTION (0.8ms)  COMMIT
11:18:58 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:29:in `update'
11:18:58 web.1  |   User Load (0.4ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
11:18:58 web.1  | Completed 200 OK in 25ms (Views: 0.3ms | ActiveRecord: 4.1ms | Allocations: 7453)
```

***destroy***

```terminal
11:19:27 web.1  | Started DELETE "/api/v1/posts/5" for ::1 at 2021-08-16 11:19:27 +0900
11:19:27 web.1  | Processing by Api::V1::PostsController#destroy as HTML
11:19:27 web.1  |   Parameters: {"id"=>"5"}
11:19:27 web.1  |   User Load (0.7ms)  SELECT `users`.* FROM `users` WHERE `users`.`uid` = 'test@example.com' LIMIT 1
11:19:27 web.1  |   Post Load (0.4ms)  SELECT `posts`.* FROM `posts` WHERE `posts`.`id` = 5 LIMIT 1
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:42:in `destroy'
11:19:27 web.1  |   TRANSACTION (0.1ms)  BEGIN
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:44:in `destroy'
11:19:27 web.1  |   DetailInfo Load (0.3ms)  SELECT `detail_infos`.* FROM `detail_infos` WHERE `detail_infos`.`post_id` = 5 LIMIT 1
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:44:in `destroy'
11:19:27 web.1  |   DetailInfo Destroy (0.2ms)  DELETE FROM `detail_infos` WHERE `detail_infos`.`id` = 5
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:44:in `destroy'
11:19:27 web.1  |   Post Destroy (0.2ms)  DELETE FROM `posts` WHERE `posts`.`id` = 5
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:44:in `destroy'
11:19:27 web.1  |   TRANSACTION (0.2ms)  COMMIT
11:19:27 web.1  |   ↳ app/controllers/api/v1/posts_controller.rb:44:in `destroy'
11:19:27 web.1  |   User Load (0.3ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
11:19:27 web.1  | Completed 200 OK in 13ms (Views: 0.1ms | ActiveRecord: 2.3ms | Allocations: 5164)
```

### 最後に

part1 ~ part4をReact × Rail APIアプリケーションの基本的な処理を一通り記事にまとめてみました
次はredux × redux sagaのステート管理とTypeScript導入をやってみようと思っています

##### おわり