Rails Join Table - Step by Step Guide to Create a Favoriting Feature in an Ecommerce App
Tags: react, programming, javascript, rails
Published: 2022-06-03 02:20:16

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)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ghr4f3osdy5qt2qs5tlf.png)

## 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:

![heart icon is showing under image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ct66ohb5a2hf14i9txuh.png)

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.

![list of favored items](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bjgx8jn2fhjbetopksk2.png)

## 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.