Creating a “Follow” button: from Rails to React using Self-Join
Tags: tutorial, rails, react, database
Published: 2022-06-15 23:02:04

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:

```other
// 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
```

![Database table chart](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h8sozu05wr5ask6x4oiz.jpeg)

After reading [this](https://medium.com/@ingridf/the-power-of-self-referential-associations-in-rails-and-self-joins-a9d31b181e4) and [this](https://medium.com/@TheDickWard/self-referential-relationships-aka-self-joins-in-rails-64f8f36ac311), in addition to the [Rails guide](https://guides.rubyonrails.org/association_basics.html#self-joins), 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.

```other
// app/models/friendship.rb

class Friendship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followee, class_name: "User"
end
```

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.

```other
// 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
```

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.

```other
// 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
```

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](https://medium.com/@TheDickWard/self-referential-relationships-aka-self-joins-in-rails-64f8f36ac311) 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

```other
// 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;
```

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

* [https://medium.com/@TheDickWard/rails-relationships-a-user-by-any-other-name-c6c9f0adc972](https://medium.com/@TheDickWard/rails-relationships-a-user-by-any-other-name-c6c9f0adc972)
* [https://medium.com/full-taxx/how-to-add-likes-to-posts-in-rails-e81430101bc2](https://medium.com/full-taxx/how-to-add-likes-to-posts-in-rails-e81430101bc2)
* [https://medium.com/@klee.mcintosh/setting-up-a-self-join-with-rails-activerecord-9137062fac8b](https://medium.com/@klee.mcintosh/setting-up-a-self-join-with-rails-activerecord-9137062fac8b)
* [https://www.kartikey.dev/2020/09/29/many-to-many-self-joins-in-rails.html](https://www.kartikey.dev/2020/09/29/many-to-many-self-joins-in-rails.html)
* [https://stackoverflow.com/questions/25493368/many-to-many-self-join-in-rails](https://stackoverflow.com/questions/25493368/many-to-many-self-join-in-rails)
* [https://medium.com/@asuthamm/self-join-in-rails-8e3fc99c0634](https://medium.com/@asuthamm/self-join-in-rails-8e3fc99c0634)
* [https://flatironschool.com/blog/self-referential-associations-aka-self-joins/](https://flatironschool.com/blog/self-referential-associations-aka-self-joins/)
* [https://tmtarpinian.com/self-joins-in-rails/](https://tmtarpinian.com/self-joins-in-rails/)
* [https://betterprogramming.pub/building-self-joins-and-triple-joins-in-ruby-on-rails-455701bf3fa7](https://betterprogramming.pub/building-self-joins-and-triple-joins-in-ruby-on-rails-455701bf3fa7)