react,railsのカート機能の作成方法
背景
個人開発でECサイトを作成しており、ユーザーが選択した商品をカートに維持させるためのカート機能を実装しました。
カート機能を作成するために各モデルをどのような設計にすべきか作成するのに時間がかかりましたが、これからカート機能を作成しようとしている方の参考になると思い、実装方法を記事にまとめてみました。
よかったら参考にしていただけると幸いです。
完成イメージ
ER図
仕様
カートは、ユーザーが商品をカートに追加した時作成されるものとしました。
つまり、今回は上記の完成イメージの商品詳細ページの「カートに入れる」ボタン押した時にカートが作成されるようにしました。
react側の実装
まず、フロント側(react側)の実装について説明します。
フロント側(react側)の実装では、Item情報の取得と表示、カートに追加したいItemの数量を選択するプルダウン、ItemをCartに入れるための処理を実行・開始するためのボタンなどを作成しました。
ItemDetail.jsx (商品詳細ページ)
import React, { useState, useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux';
import { useParams, useNavigate } from 'react-router-dom';
import Typography from '@mui/material/Typography';
import logo from '../..//20230416_シャンプー画像.jpg'
import { Button, MenuItem } from '@mui/material';
import { Field, reduxForm } from 'redux-form';
// components
import ItemDropDownForm from './ItemDropDownForm';
// api
import { fetchItemsDetail } from '../../apis/itemsDetail'
import { fetchUserData } from '../../apis/fetchUserDara';
// function
import { createCart } from '../../apis/createCart';
// modules
import { setupAxiosHeaders, createAPIInstance } from '../../modules/accessUserData';
const ItemDetail = () => {
const [item, setItem] = useState()
const params = useParams();
const id = params.id
const navigate = useNavigate();
const dispatch = useDispatch()
const form = useSelector(state => state.form);
const values = form && form.orderForm && form.orderForm.values;
// 商品数の選択範囲
const options = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10
];
// 商品詳細データの取得
useEffect(() => {
fetchItemsDetail(id)
.then((data) =>
setItem(data.items)
)
}, [id])
// ログインユーザーの取得
useEffect(() => {
const accessToken = localStorage.getItem('access-token');
const client = localStorage.getItem('client');
const uid = localStorage.getItem('uid');
setupAxiosHeaders(accessToken, client, uid);
const api = createAPIInstance(accessToken, client, uid);
fetchUserData(api.defaults.headers.common, dispatch)
}, [dispatch])
// カートの作成とカートに商品追加
const InsertItemToCart = () => {
// 商品の名前と数量のデータ
const ItemName = item && item.name
const quantity = values.quantity
createCart(ItemName, quantity)
}
return (
<div className="contents">
<br></br>
<div className="wrapper">
<div className="column cat1">
<div className="info">
<img src={logo} alt="シャンプー画像" />
</div>
</div>
<div className="column cat2">
<div className="info">
<Typography variant="body2" color="text.secondary">
商品名
</Typography>
<div>{item && item.name}</div>
<br></br>
<Typography variant="body2" color="text.secondary">
特徴
</Typography>
<div>{item && item.description}</div>
<br></br>
<Typography variant="body2" color="text.secondary">
価格
</Typography>
<div>¥{item && item.price}</div>
<br></br>
<Typography variant="body2" color="text.secondary">
在庫数
</Typography>
<div>{item && item.stock}</div>
</div>
<br></br>
<Field name="quantity" component={ItemDropDownForm} label="数量">
{options.map(option => (
<MenuItem key={option} value={option}>
{option}
</MenuItem>
))}
</Field>
<br></br>
<br></br>
<Button variant="outlined" onClick={InsertItemToCart}>カートに入れる</Button>
<Button variant="outlined" style={{ margin: '3mm'}} onClick={() => navigate('/order')}>ご注文手続きへ</Button>
</div>
</div>
</div>
)
}
// export default ItemDetail
export default reduxForm({
form: 'orderForm',
})(ItemDetail);
上記のコードは、Reactコンポーネント ItemDetail
を定義しています。ItemDetail
コンポーネントは、Itemの詳細情報を表示し、CartにItemを追加するための機能を提供します。
frontend/src/modules/accessUserData.js (アクセストークンなどからユーザー情報へアクセスするための準備)
import axios from 'axios';
export const setupAxiosHeaders = (accessToken, client, uid) => {
axios.defaults.headers.common['access-token'] = accessToken;
axios.defaults.headers.common['client'] = client;
axios.defaults.headers.common['uid'] = uid;
};
export const createAPIInstance = (accessToken, client, uid) => {
const headers = {
'access-token': accessToken,
'client': client,
'uid': uid
};
const api = axios.create({
baseURL: 'http://localhost:3010/api/v1',
headers: {
'Content-Type': 'application/json',
...headers
}
});
return api;
};
上記のコードは、axiosを使用してAPIリクエストを行うための設定を行っています。
(上記のコードでなぜ localStorage から取得したアクセストークンなどの情報を用いてユーザー情報を取得しているのかというと、新規登録時やログイン時にdevise-token-authを用いて得られたアクセストークンなどの情報をlocalStorageに保存しており、その情報を用いて認証を行い、認証成功したらAPIを実行できるようにしているからです。)
setupAxiosHeaders
関数は、アクセストークン(accessToken)、クライアント(client)、およびユーザーID(uid)を受け取り、axiosのデフォルトのヘッダーにこれらの値を設定します。これにより、後続のすべてのaxiosリクエストでこれらのヘッダーが自動的に送信されます。
createAPIInstance
関数は、アクセストークン(accessToken)、クライアント(client)、およびユーザーID(uid)を受け取り、これらの値をヘッダーに設定したaxiosのインスタンス(api)を作成します。また、ベースURL(baseURL)を指定し、Content-Typeヘッダーに'application/json'を設定します。
これにより、作成されたaxiosインスタンスは指定されたベースURLに対してJSON形式のリクエストを行う準備を設定しています。
これらの関数を使用することで、axiosを利用したAPIリクエストを容易に行うことができます。設定したヘッダーは、リクエストごとに明示的に指定する必要はなくなります。
frontend/src/apis/fetchUserDara.js (ログインユーザー情報の取得)
import axios from 'axios';
import { userDataUrl } from '../urls/index'
import { dispatchUserData } from '../reducks/reducers/user';
// ログインユーザー情報の取得
export const fetchUserData = async(userData, dispatch) => {
await axios.post(userDataUrl, userData)
.then(data => {
dispatch(dispatchUserData(data));
}).catch(error => {
console.log(error);
});
};
上記のコードは、axiosを使用してサーバーからユーザーデータを取得するための関数 fetchUserData
を定義しています。
fetchUserData
関数は、userData
(ユーザーデータ)と dispatch
(アクションを送信するための関数)を引数として受け取ります。
関数の本体では、axios.post
メソッドを使用して、指定された userDataUrl
(ユーザーデータを取得するためのエンドポイントのURL)に対して POST リクエストを行います。userData
はリクエストのペイロードとして渡されます。
.then
ブロックでは、リクエストが成功した場合に実行されるコールバック関数が定義されています。受け取ったデータ(data
)はコンソールに出力され、さらに dispatchUserData
アクションが dispatch
関数を介してディスパッチされます。これにより、ユーザーデータをアプリケーションの状態に反映させることができます。
.catch
ブロックでは、リクエストが失敗した場合に実行されるコールバック関数が定義されています。エラーオブジェクト(error
)はコンソールに出力されます。
つまり、このコードは、指定されたエンドポイントに対してユーザーデータを送信し、サーバーからの応答を受け取ります。その後、受け取ったデータをコンソールに出力し、アプリケーションの状態を更新するために dispatchUserData
アクションをディスパッチします。
frontend/src/apis/createCart.js (カート作成と商品の追加)
import axios from 'axios';
import { createCarturl } from '../urls/index'
// カート作成と商品追加
export const createCart = async(ItemName, quantity) => {
const params = { name: ItemName, quantity: quantity }
await axios.post(createCarturl, params)
.then(data => {
console.log(data)
}).catch(error => {
console.log(error);
});
};
上記のコードは、axiosを使用してサーバーに対してカートを作成し、商品を追加するための関数 createCart
を定義しています。
createCart
関数は、ItemName
(商品名)と quantity
(数量)を引数として受け取ります。
関数の本体では、params
オブジェクトを作成し、その中に ItemName
と quantity
の値を格納します。
次に、axios.post
メソッドを使用して、指定された createCarturl
(カート作成と商品追加のためのエンドポイントのURL)に対して POST リクエストを行います。リクエストのボディとして params
オブジェクトが渡されます。
.then
ブロックでは、リクエストが成功した場合に実行されるコールバック関数が定義されています。受け取ったデータ(data
)はコンソールに出力されます。
.catch
ブロックでは、リクエストが失敗した場合に実行されるコールバック関数が定義されています。エラーオブジェクト(error
)はコンソールに出力されます。
まとめると、上記のコードは、指定されたエンドポイントに対して商品名と数量を含むパラメータを送信し、サーバーにカートの作成と商品の追加をリクエストします。成功した場合、サーバーからの応答データがコンソールに出力されます。失敗した場合、エラーがコンソールに出力されます。
rails側の実装
次にカート機能を実装するためrails側の処理を実装する方法を紹介します。
まず、各モデルの関連付けを行います。
app/models/cart.rb
class Cart < ApplicationRecord
belongs_to :user
has_many :cart_items, dependent: :destroy
has_many :items, through: :cart_items
end
app/models/cart_item.rb
class CartItem < ApplicationRecord
belongs_to :cart
belongs_to :item
end
app/models/item.rb
class Item < ApplicationRecord
has_many :cart_items
has_many :carts, through: :cart_items
end
次にルーティングを設定します。
config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
mount_devise_token_auth_for 'User', at: 'auth', controllers: {
registrations: 'api/v1/auth/registrations',
sessions: 'api/v1/auth/sessions'
}
namespace :auth do
resources :sessions, only: [:index]
end
resources :users
post 'users/fetch_userdata', to: 'users#fetch_userdata'
resources :carts
resources :items
end
end
end
上記のコードは、Railsのルーティング設定を定義しています。
この設定により、APIのエンドポイントが適切に定義され、各エンドポイントへのリクエストに対して適切なコントローラーとアクションが関連付けられるようになります。
最後に、controllerでカート(Cart)を作成し、さらにCartItemという選択した商品(Item)とカート(Cart)を紐付け管理するための中間テーブルを作成します。
app/controllers/api/v1/carts_controller.rb
module Api
module V1
class CartsController < ApplicationController
before_action :authenticate_api_v1_user!
# カートの作成と商品の追加
def create
cart = current_api_v1_user.cart || Cart.create(user_id: current_api_v1_user.id)
item = Item.find_by(name: params[:name])
response = {}
if item
cart_item = CartItem.create(cart_id: cart.id, item_id: item.id, quantity: params[:quantity])
item.update(stock: item.stock - params[:quantity]) if cart_item.valid?
response[:cart] = cart
response[:item] = item
response[:message] = "商品がカートに正常に追加されました。"
else
response[:error] = "商品が見つかりません。"
end
render json: response, status: :ok
end
end
end
end
上記のコードで行っていることを解説します。
- 最初に、現在のログインユーザーに関連付けられたCartを取得します。もしCartが存在しない場合、
Cart.create(user_id: current_api_v1_user.id)
を使用して新しいCartを作成します。(current_api_v1_user
メソッドは、Railsアプリケーション内での認証済みのユーザーを表すヘルパーメソッドです。このヘルパーメソッドは、devise-token-authなどを使用して認証されたユーザーの情報にアクセスするために使用されます。) -
params[:name]
に指定された商品の名前で Item を検索します。 - Itemが見つかった場合、
CartItem.create
を使用してCartItemを作成します。このとき、カートID、アイテムID、および数量が指定されます。 - もし CartItem が正常に作成された場合、アイテムの在庫数を更新します。具体的には、
item.stock
からparams[:quantity]
を減算し、item.update(stock: updated_stock)
を呼び出します。 - レスポンスに含める情報を保持するために、
response
という空のハッシュを作成します。 -
item
が存在する場合、response
にcart
オブジェクト、item
オブジェクト、および成功メッセージを格納します。 -
item
が存在しない場合、response
にエラーメッセージを格納します。 -
response
をJSON形式でレスポンスとして返します。HTTPステータスコードとしては:ok
を指定します。
要約すると上記のコードは、 User の Cart に Item を追加し、Itemの在庫数から選択したItemの個数差し引く機能を持っています。また、処理の結果に応じた情報を含むJSON形式のレスポンスを返すようにしています。成功した場合はCartオブジェクトとItemオブジェクト、エラーが発生した場合はエラーメッセージがレスポンスに含まれます。
まとめ
今回はUIの部分をreact側で実装し、rails側でカート機能を実装する方法を紹介しました。
カート機能を作成してみて実感したことは、モデルの関連付けを適切に理解していないと実装することは難しいということです。
カート機能を実装しようとしたとき、関連付けがうまくいかずとても苦戦しました。
今後はより適切な関連付けができるように、rails側で機能を実装する時にどのようなモデル同士の関係性を保つべきかを意識しながら実装していきます。
参考
Discussion