How I can "like" posts in my Instagram-esque rails/react/redux app
Kevin Kirby
Posted on October 11, 2021
For my final project at Flatiron School, I created a Photo Sharing app with some similarities to instagram. I utilized rails for the backend and react along with redux for the frontend.
For my backend models, I initially just had User, Post and Comment.
In deciding to add functionality to like a post, I first had to make backend changes.
In thinking relationships, users and posts can have many likes. Thus, likes belong to users and likes belong to posts. I added a like model and migration file by running the following command: rails g model Like user_id:integer post_id:integer
That gave me my model and migration file. I added these two relationship lines to my like model:
belongs_to :user
belongs_to :post
Next, because of these belongs_to relationships, I added has_many :likes
to both my User and Post models.
Then, I ran rake db:migrate to create the likes table in the database.
Knowing I'd need to update config/routes, I personally decided to add resources :likes nested underneath my resources :posts like so:
resources :posts do
resources :likes
end
Next I added a basic likes controller (I will fill this in later into this blog):
class Api::V1::LikesController < ApplicationController
def create
end
end
I then added :likes to the attributes list of my user serializer and post serializer, both serializers being created with ActiveModel serializer.
The backend part is now done except for a few additional lines that need to go in the Likes controller. For now let's hop to what I did on the front-end, and then I'll come back to that.
So I already had a Post component, a class component to be specific, which represents how each post should look and behave.
I wanted to add a like button in the Post component so it showed right under each post.
So I added the below function, above my render function. This function will check to see if a given post, passed from props by the Posts component has no likes where the user_id of that like is equal to the the current user's id... if that is true (meaning the current user has not liked the post) I want to return an unfilled like icon, but if false (meaning the current user has liked the post) I want to return a filled like icon:
handle_likes = () => {
if (
!this.props.post.likes.some(
(like) => like.user_id === this.props.currentUser.id
)
) {
return (
<Icon
icon="fluent:heart-20-regular"
width="30"
height="30"
className="likeButton"
/>
);
} else {
return (
<Icon
icon="fluent:heart-20-filled"
width="30"
height="30"
className="unlikeButton"
color="#dc565a"
/>
);
}
};
I brought the icons in from Iconify and to do so just had to put this import at the top of my Post component:
import { Icon } from "@iconify/react";
And I also had to put this line in my render function's return statement, so the above function would be called on render:
{this.handle_likes()}
I then knew I wanted stuff to happen when I clicked the first icon, the unfilled like icon. Thus, I wrote the code I wanted to happen.
I knew on the click of this icon I wanted to dispatch an action to the reducer which would then later update the store. I knew I should use a reduxed-up action creator called addLike in this onClick handler and that it should take as arguments the current user's id and the post id. Here is the onClick event handler I added to the bottom of the icon:
<Icon
icon="fluent:heart-20-regular"
width="30"
height="30"
className="likeButton"
onClick={() =>
this.props.addLike(this.props.currentUser.id, this.props.post.id)
}
/>
Now let's add an import for this addLike action creator that I will be creating in a little, and add that action creator I'll soon be making to connect. This way our component will have as a prop a reduxed-up version of the action creator so that we can dispatch an action to the reducer when the icon is clicked:
I added these two imports:
import { addLike } from "../actions/allPosts";
import { connect } from "react-redux";
I added what will soon be the addLike action creator to the second argument of connect at the bottom of my component:
export default connect(null, { removePost, deleteLike, addLike })(Post);
Then I built this aforementioned action creator.
I decided to put this action creator in my action creators file that encompasses all the action creators for my posts, since I wanted likes of posts to be housed within individual posts within my posts part of state.
This is the addLike action creator I put together:
export const addLike = (user_id, post_id) => {
return (dispatch) => {
return fetch(`http://localhost:4500/api/v1/posts/${post_id}/likes`, {
credentials: "include",
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user_id),
})
.then((res) => res.json())
.then((response) =>
dispatch({
type: "ADD_LIKE",
payload: response,
})
);
};
};
Above, I am first taking in the user_id and post_id that I passed from my onClick event handler in the Post component.
I'm then using thunk which allows me to return a function instead of an object and that function will have access to the stores dispatch method.
Within that function I'm doing a post fetch to the correct post's likes route, am getting a promise that resolves to a JavaScript object which is the like, and am then using the store's dispatch method to dispatch an object with a type of "ADD_LIKE" and the payload set to the like object to the reducer.
Now I'll cover the reducer part of this. The dispatch I made in the above action creator had a type of "ADD_LIKE". When my reducer for my posts sees an action come in with that type, it should take the previous state and this action, and with that it should determine a brand new value for the state. That new version of state should be returned and this will ultimately update the store.
FYI, here is what my state structure looks like at the top in the first argument of my reducer:
state = { posts: [] }
So, since I need to be able to handle an action for adding a like, I created a case in my reducer for "ADD_LIKE" that looks like this (explanation right below):
case "ADD_LIKE":
let posts = [...state.posts].map((post) => {
if (parseInt(post.id) === action.payload.post_id) {
let updatedPost = {
...post,
likes: [...post.likes, action.payload],
};
return updatedPost;
} else {
return post;
}
});
return { ...state, posts: posts };
Essentially in this code I am:
-Setting a variable of posts equal to mapping through a copy of all of the posts that are currently in my posts part of state.
-For each post in the current state of posts, I'm checking to see if its post id is equal to the post_id from the like object that I sent as the payload at the end of my action creator.
-If that if statement rings true that means that means that a like happened to one of our posts.
-Since a like happened, I want this post to have the like in its like array. Thus, I create an updatedPost variable equal to an object of everything that is in this post already, but with the likes part of the post object being updated to what was already in this likes part of the object, but with the new like added in as well.
-I then return this updatedPost into the array I'm creating by mapping through all the current posts.
-Then, for all the other posts that were not liked at this time, those posts will fail the if conditional and so my else statement will just return the post itself into this array being created by .map.
-This map will then put into the posts variable an array of a bunch of posts, and one of them will have a new like within its like array.
-We then end the map statement and return an object containing what is already in the reducer's state, but set the posts key of the object equal to the posts variable that contains the array we made from mapping which will have all current posts including an updated post which is the one that was freshly liked.
To come back to the likes controller, here is what I did for that:
-I created a basic create action:
def create
end
I then put a byebug in the create action, clicked the icon to like a post, and checked what params was when I hit the byebug after the post fetch was fired.
The post_id went through to params and I used the following line to create a like with the current user's id (with help of a helper method from application controller) and the post_id for the liked post:
@like = Like.create(user_id: current_user.id, post_id: params[:post_id].to_i)
I then did render json: @like
to render the json object for this like, so all I had for the create action was this altogether:
def create
@like = Like.create(user_id: current_user.id, post_id: params[:post_id].to_i)
render json: @like
end
Lastly, this is what I have for my like_params strong parameters method:
def like_params
params.permit(:user_id, :post_id)
end
So this is how I got the liking functionality working to update both the backend and the frontend once an unfilled like button is clicked. When I got this working, the unfilled like icon would change into a filled like icon on click, and the state in redux for the liked post would change as well.
Hope this is possibly able to help someone!
Posted on October 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.