DEV Community

Nissrine Canina
Nissrine Canina

Posted on

Rails Join Table - Step by Step Guide to Create a Favoriting Feature in an Ecommerce App

In a context of a basic e-commerce app where a user can buy, list, edit, and like an item. We are going to focus on the feature where the user can view item details and click the heart icon to save the item in the favorites list. The user can view or delete items from the favorite list. In this article, I am going to walk you through the steps to set up your backend and frontend to achieve this functionality.

Step 1: Entity Relationship Diagram (ERD)

Create an ERD of three models: a user, an item, and a favorite_item where a user has many favorite_items and has many items through favorite_items. Similarly, an item has many favorites_items as well as many favorated_by (aliased users) through favorite_items. The first association (the user has many items as favorites) is what we need for the favoring feature.

Entity Relationship Diagram showing 3 models: user, item, and join table (favorite_item)

Step 2: Generate Resource and Add Associations in Rails

Use the resource command generator to create the join table of favorite items. The resource will generate the model, controller, serializer, and resource routes.

 rails g resource favorite_item user:belongs_to item:belongs_to

Enter fullscreen mode Exit fullscreen mode

Then, add has_many associations to both item and user models. Since the belongs_to association is already specified, it will be provided by rails in the favorite_item model. Then, add validations to assure that an item is only favored one time by the same user.

class User < ApplicationRecord
 has_many :favorite_items, dependent: :destroy
 has_many :items, through: :favorite_items
end
Enter fullscreen mode Exit fullscreen mode
class Item < ApplicationRecord
  has_many :favorite_items, dependent: :destroy
  has_many :favorited_by, through: :favorite_items, source: :user
end
Enter fullscreen mode Exit fullscreen mode
class FavoriteItem < ApplicationRecord
  belongs_to :user
  belongs_to :item

  validates :item_id, uniqueness: { scope: [:user_id], message: 'item is already favorited' }
end
Enter fullscreen mode Exit fullscreen mode

Next, update user and favorite_item serializers.

class UserSerializer < ActiveModel::Serializer
  has_many :favorite_items
  has_many :items
end
Enter fullscreen mode Exit fullscreen mode

In the favorite_item serializer, add :item_id attribute. This will identify which item is favored by the user.

class FavoriteItemSerializer < ActiveModel::Serializer
  attributes :id, :item_id
  has_one :user
  has_one :item
end
Enter fullscreen mode Exit fullscreen mode

Step 3: Add Methods to Controller

Add create and destroy actions to the favorite_item controller:

class FavoriteItemsController < ApplicationController
    def create 
       favorite_item = current_user.favorite_items.create(favorite_item_params)
        if favorite_item.valid?
            render json: favorite_item.item, status: :created
        else
            render json: favorite_item.errors, status: :unprocessable_entity
        end
    end



    def destroy 
        render json: FavoriteItem.find_by(item_id: Item.find(params[:id]).id, user_id: current_user.id).destroy
    end

    private

    def favorite_item_params
        params.require(:favorite).permit(:item_id, :user_id)
    end


end

Enter fullscreen mode Exit fullscreen mode

Also, make sure to specify routes in the routes.rb file as such: resources :favorite_items, only: [:create, :destroy]

Step 4: Frontend React Side - Add Favorite

The favoriting icon is showing when the user is viewing item details:

heart icon is showing under image

In the selected item component, add the heart icon:

<div >
 <Icon onClick={() => addFavorite(selectedItem) } 
 color="red" name="heart outline" />
</div>
Enter fullscreen mode Exit fullscreen mode

The addFavorite(selectedItem) is a callback function defined at the highest level App.jsx:

const addFavorite = (item) => {
    const newFavorite = {
      favorite: {
        item_id: item.id, user_id: currentUser.id
      }
    }
    fetch("/favorite_items", { 
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(newFavorite),
    })
    .then(resp => {
      if (resp.ok) {
        return resp.json()
      } else {
        return resp.json().then(errors => Promise.reject(errors))
      }
    })
    .then((newFav) => {
      setFavorites([...favorites, newFav])
      navigate("/items")
     })
  }

Enter fullscreen mode Exit fullscreen mode

When you click on the heart icon, you will be redirected back to the main list of items for sale. The favored item/s can be viewed via the favorites button in the navigation bar.

list of favored items

Step 5: Frontend React Side - Remove Favorite

Create the favorite items' container and reuse ItemCard component when you map through favorite items:

import React from 'react'
import ItemCard from '../components/ItemCard'
import { Container, Card } from 'semantic-ui-react'

const Favorites = ({ favorites, removeFavorite }) => {

  return (
    <Container textAlign="center">
      {favorites.length === 0 ? <h2 style={{ paddingTop: '50px' }}>You have no favorites!</h2> :
      <>
      <div>
        <h1>The items you liked!</h1>
      </div>
      <div className="ui divider">
        <Card.Group itemsPerRow={3}> 
          {favorites.map((item) => (

            <ItemCard 
             key={item.id}
             item={item}
             removeFavorite={removeFavorite}
             redHeart={true}
            />
          ))}

        </Card.Group>
      </div>
      </>
}
    </Container>
  )
}

export default Favorite
Enter fullscreen mode Exit fullscreen mode

Use props to display the red heart icon in ItemCard component:

import React from 'react'
import { Card, Image, Icon } from 'semantic-ui-react'
import {useNavigate} from 'react-router-dom'
const ItemCard = ({ item, removeFavorite, redHeart }) => {

  const navigate = useNavigate()

  const handleClick = () => {
      navigate(`/items/${item.id}`)
  }

   return (
    <div className="item-card">
        <Card color='blue' >

        <div onClick={handleClick} className="image" >
          <Image src={item.image} alt={item.name} wrapped />     
         </div>

           <Card.Content>
                <Card.Header>{item.name}</Card.Header>
                <Card.Description>{item.price}</Card.Description>

            </Card.Content>
            <br />
            {redHeart ? (
              <span onClick={() => removeFavorite(item)}>
                <Icon color="red" name="heart" />
              </span>
            ) : null }
        </Card>
    </div>
  )
}

export default ItemCard
Enter fullscreen mode Exit fullscreen mode

When the user clicks the red heart icon, it will run the callback function removeFavorite(item). This function is defined in the highest level component App.jsx:

const removeFavorite = (item) => {
    const foundFavorite = favorites.find((fav) => fav.id === item.id)

   return  fetch(`/favorite_items/${foundFavorite.id}`, {
      method: "DELETE"
    })
    .then(resp => resp.json())
    .then(() => {
      const filteredFavorites = favorites.filter((fav) => fav.id !== foundFavorite.id)
        setFavorites(filteredFavorites)
    })
  }
Enter fullscreen mode Exit fullscreen mode

Step 6: Update Login/Authentication State

In this project, session cookies were used to log the user in. Therefore, you need to update the state when you sign up, log in, and refresh respectively:

function handleSubmit(e) {
    e.preventDefault();

    const userCreds = { ...formData }
    fetch("/signup", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(userCreds),
    })
    .then((resp) => resp.json())
    .then((user) => {
      console.log(user)
      setFormData({
        email: "",
        username: "",
        password: "",
        passwordConfirmation: ""
      })
      setCurrentUser(user)
      setAuthenticated(true)
      setFavorites(user.items)
      navigate("/items")
    })
  }
Enter fullscreen mode Exit fullscreen mode
function handleSubmit(e) {
    e.preventDefault();

    const userCreds = { ...formData };

    fetch("/login", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(userCreds),
    })
      .then((r) => r.json())
      .then((user) => {
         setCurrentUser(user)
          setAuthenticated(true)
          setFavorites(user.items)
        setFormData({
          username: "",
          password: "",
        });
        navigate("/items")
      });
  }

Enter fullscreen mode Exit fullscreen mode
useEffect(() => {
    fetch("/me", {
      credentials: "include",
    })
    .then((res) => {
      if (res.ok) {
        res.json().then((user) =>{ 
          setCurrentUser(user)
          setAuthenticated(true)
          setFavorites(user.items)
        });
      } else {
        setAuthenticated(true)
      }
    });
Enter fullscreen mode Exit fullscreen mode

Conclusion

This example concludes one of the possible ways to implement favoring an object from a list and displaying a new list of favorite objects using rails join table associations.

Top comments (0)