Rails Join Table - Step by Step Guide to Create a Favoriting Feature in an Ecommerce App
Nissrine Canina
Posted on June 3, 2022
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.
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
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
class Item < ApplicationRecord
has_many :favorite_items, dependent: :destroy
has_many :favorited_by, through: :favorite_items, source: :user
end
class FavoriteItem < ApplicationRecord
belongs_to :user
belongs_to :item
validates :item_id, uniqueness: { scope: [:user_id], message: 'item is already favorited' }
end
Next, update user and favorite_item serializers.
class UserSerializer < ActiveModel::Serializer
has_many :favorite_items
has_many :items
end
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
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
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:
In the selected item component, add the heart icon:
<div >
<Icon onClick={() => addFavorite(selectedItem) }
color="red" name="heart outline" />
</div>
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")
})
}
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.
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
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
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)
})
}
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")
})
}
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")
});
}
useEffect(() => {
fetch("/me", {
credentials: "include",
})
.then((res) => {
if (res.ok) {
res.json().then((user) =>{
setCurrentUser(user)
setAuthenticated(true)
setFavorites(user.items)
});
} else {
setAuthenticated(true)
}
});
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.
Posted on June 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.