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.



    
Top comments (0)