🐈

react,railsのカート機能の作成方法

2023/06/03に公開

背景

個人開発で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 オブジェクトを作成し、その中に ItemNamequantity の値を格納します。

次に、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

上記のコードで行っていることを解説します。

  1. 最初に、現在のログインユーザーに関連付けられたCartを取得します。もしCartが存在しない場合、Cart.create(user_id: current_api_v1_user.id) を使用して新しいCartを作成します。(current_api_v1_user メソッドは、Railsアプリケーション内での認証済みのユーザーを表すヘルパーメソッドです。このヘルパーメソッドは、devise-token-authなどを使用して認証されたユーザーの情報にアクセスするために使用されます。
  2. params[:name] に指定された商品の名前で Item を検索します。
  3. Itemが見つかった場合、CartItem.create を使用してCartItemを作成します。このとき、カートID、アイテムID、および数量が指定されます。
  4. もし CartItem が正常に作成された場合、アイテムの在庫数を更新します。具体的には、item.stock から params[:quantity] を減算し、item.update(stock: updated_stock) を呼び出します。
  5. レスポンスに含める情報を保持するために、response という空のハッシュを作成します。
  6. item が存在する場合、responsecart オブジェクト、item オブジェクト、および成功メッセージを格納します。
  7. item が存在しない場合、response にエラーメッセージを格納します。
  8. response をJSON形式でレスポンスとして返します。HTTPステータスコードとしては :ok を指定します。

要約すると上記のコードは、 User の Cart に Item を追加し、Itemの在庫数から選択したItemの個数差し引く機能を持っています。また、処理の結果に応じた情報を含むJSON形式のレスポンスを返すようにしています。成功した場合はCartオブジェクトとItemオブジェクト、エラーが発生した場合はエラーメッセージがレスポンスに含まれます。

 

まとめ

今回はUIの部分をreact側で実装し、rails側でカート機能を実装する方法を紹介しました。
カート機能を作成してみて実感したことは、モデルの関連付けを適切に理解していないと実装することは難しいということです。
カート機能を実装しようとしたとき、関連付けがうまくいかずとても苦戦しました。
今後はより適切な関連付けができるように、rails側で機能を実装する時にどのようなモデル同士の関係性を保つべきかを意識しながら実装していきます。

 

参考

https://railsguides.jp/association_basics.html

Discussion