DEV Community

Cover image for Creating a “Follow” button: from Rails to React using Self-Join
Julie Evans
Julie Evans

Posted on • Edited on

Creating a “Follow” button: from Rails to React using Self-Join

For the first time, I attempted creating a button that a user could click to follow or unfollow another user.

Database and Self-Join

First of all, the database is set up as a many-to-many relationship. But, it is also self-referential, otherwise known as a self-join. This means that the data in the database uses another table (in this case “Friendship”) to reference a table with itself (in this case “User”). More on this later…

Here is the schema and a diagram to demonstrate these database tables:

// db/schema.rb

  create_table "friendships", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "follower_id"
    t.integer "followee_id"
  end

  create_table "users", force: :cascade do |t|
    t.string "username"
    t.string "password_digest"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end
Enter fullscreen mode Exit fullscreen mode

Database table chart

After reading this and this, in addition to the Rails guide, I got an understanding of Self-Join. In the case I used it, based on this, the database needs to be set up just like a many-to-many relationship. It needs a separate table to act as a go-between and store the action’s information. The only difference would be that instead of this separate table in between two other tables, it would have one other table referencing itself.

User >- Friendship -< User
(the User here is the same table - just the connection would look like this)

instead of something like this…

User >- Like -< Post

Another way to look at it is that the Friendship table holds the information pertaining to the “follow” action in this case, and that info contains the id of the user that initiated the action as a foreign key as well as the id of the user the action was initiated upon also as a foreign key. It has two user ids, one in the follower column and the other in the followee column.

The Friendship database table would look something like this:

follower_id followee_id
1 2
2 1

NOTE: For clarification, the first row has the user with the id of 1 following the user with the id of 2, and the second row has the user with the id of 1 being followed by the user with the id of 2. Also, the ids of the users are the primary keys from the User table. On a side note, this table would also have another column for the primary key of the relationship or row in the Friendship table.

Rails Backend

On the back-end of things, there are primarily two things, the models and the controllers concerning this Friendship table as well as the User table.

The most important part of this whole thing lies in the models, especially the User model. Both of these models set up the connections for the tables in the database. The Friendship model essentially allows the follower and followee to be present and that they come from the User model.

// app/models/friendship.rb

class Friendship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followee, class_name: "User"
end
Enter fullscreen mode Exit fullscreen mode

The User model is a bit more complicated. It needs the send the follower and followee to the Friendship model. In order to do that it needs to define them.

// app/models/user.rb

class User < ApplicationRecord
  has_many :followed_users, foreign_key: :follower_id, class_name: "Friendship"
  has_many :followees, through: :followed_users
  has_many :following_users, foreign_key: :followee_id, class_name: "Friendship"
  has_many :followers, through: :following_users
end
Enter fullscreen mode Exit fullscreen mode

Here the followers and followees are the same as the follower and followee from the Friendship model. They are defined using has_many, through:. The key that it is through here is from another has_many which is defined using foreign_key:. This foreign key is the key used to define the column in the Friendship table from the database, which were follower_id and followee_id. These are from the Friendship table and are the foreign keys that are defined clearly in the model here with the has_many statement. The has_many, through: statements are simply to allow access to these foreign keys in the Friendship model under new labels for clarity.

The controllers define the actions of the models. Here the Friendship controller is the more important one. It defines the adding and deleting of data to the Friendship table, or in other words the creation and deletion of new rows in the table.

// app/controllers/friendships_controller.rb

class FriendshipsController < ApplicationController

  def create
    friendship = Friendship.create!(friendship_params)
    render json: friendship, status: :created
  end

  def destroy
    friendship = Friendship.find_by!({follower_id: session[:user_id], followee_id: params[:id]})
    friendship.destroy
    head :no_content
  end

  private

  def friendship_params
    params.permit(:id, :followee_id, :follower_id)
  end

end
Enter fullscreen mode Exit fullscreen mode

In the methods defined here, the Friendship class must be used to define the action. If the action is defined using the User class (ex. User.followers.create!) the action of this method will most likely trigger the creation or deletion of new users, rather than the creation or deletion of a relationship between them (aka. a following or unfollowing).

NOTE: Using User.followers or User.followees will work in the console will the shovel method. Look at this for more information or examples on this.

Also, the parameters or params used are the foreign keys directly from the table rather than the new labels from the model. In addition, the params used for the delete method use the user id saved in the session since this action can only be performed when there is a user logged in anyways, and the other params are from the fetch request route that has the :id of the user being followed. The delete here grabs both of these in an object as the follower_id and followee_id respectively. This is so the find_by will target the whole row in the Friendship table that has the exact same information.

The creation method is similar, except that this object is defined in the body of the fetch request instead.

React Frontend

// client/src/components/FollowBtn.js

function FollowBtn({currentUser, user, onError, onFollow, isFollowing}) {

  function handleFollow(e) {
    const following = currentUser ? {followee_id: user.id, follower_id: currentUser.id} : null

    isFollowing ? (
      fetch(`/friendships/${user.id}`, {
        method: "DELETE",
        headers: {
          "Content-Type": "application/json",
        }
      })
        .then((r) => {
          onFollow(false)
        })
      ) : (
        fetch("/friendships", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(following)
        })
          .then((r) => {
            if (r.ok) {
              r.json().then((data) => {
                onFollow(true)
              })
            } else {
              r.json().then((err) => onError(err.errors))
            }
          })
      )
  }

  return(
    <>
      <button onClick={handleFollow} className="button">{isFollowing ? "Unfollow" : "Follow"}</button>
    </>
  )
}

export default FollowBtn;
Enter fullscreen mode Exit fullscreen mode

This is a lot, but this is the component for the Follow button that includes the function on the front-end that handles the clicking of the button.

It takes the currentUser and user variables from its parent. The currentUser is the variable fetched from the user saved in the session, in other words, whoever is logged in on the browser. The user is from the data that the button is set on, which also makes it whoever the follow is attached to or the one to be followed. If there is a current user or a user logged in, the following variable is an object containing the id of the user as the followee_id and the id of the current user as the follower_id.

The following variable is the object for the body of the fetch request. The isFollowing variable is a boolean that checks to see if the current user is already following this user. If they are already following them, then the click on the button will go to the DELETE fetch request, otherwise it will go to the POST fetch request. The DELETE fetch sends the request to the /:id/unfollow route I have defined, but this requires the id of the user that this button is attached to. The POST fetch does not require the id because it is in the body of the request.

The onFollow sends the data of these actions to the parent, which controls the isFollowing variable as well as whether the button displays “FOLLOW” or “UNFOLLOW”. The onError similarly sends data to the parent, but it only sends the /errors/ if there are any. This was simply for display convenience, because it looked better in the parent rather than inside the button itself 😅.

For more references:

These are some articles I found on this topic, or at least a part of or similar topics. Mostly revolving around self-join (since this was uncharted territory for me).

Top comments (0)