Simple Instagram app with Rails 6 API and React. Part 3

In this part, we will be adding comments to our app.

Part 2 is here

First we need to create a comments table in our db. We would then give it a reference to our Pictures table so that each comment can belong to a picture. We also want to keep track of the names of people who leave a comment. We will skip tests in this part.

Let’s start.

Checkout to a new branch. Call it comments. Then create a comments model that has a name and comment column.

rails g model Comment name:string comment:string

Let’s add some validations. A comment should always have a name and a comment and it should belong to a picture

app > models > comment.rb
class Comment < ApplicationRecord
validates_presence_of :name, :comment
belongs_to :picture
end

We also need to update our Picture model

class Picture < ApplicationRecord
validates_presence_of :img_link, :created_by
has_many :comments
end

Next we need a controller for us to get and send data to our model. We probably won’t get through writing all the code for this methods but we will have them in our controller if we decide to further develop the app in the future.

rails g controller comments index show create update destroy

In your routes.rb file, delete the ‘get’ routes generated and add the following. We want to make the comments route off the pictures.

Rails.application.routes.draw do
resources :pictures, only: [:show, :destroy, :index, :create, :update] do
resources :comments, only: [:index, :show, :create, :update, :destroy]
end
end

We forgot to add the reference to the pictures on the comments table. Not to worry. There’s a way around that.

rails g migration add_pictures_to_comments picture:references
rails db:migrate

Check the db >schema.rb and you should see a reference to our pictures on the comments table.

Image for post
Image for post

Now let’s write our controller actions.

class CommentsController < ApplicationController
before_action :set_picture #find a picture before running any of the actions here
before_action :set_picture_comment, only: [:show, :destroy]
# GET pictures/pid/comments
def index
render json: @picture.comments, status: :ok
end
# GET pictures/pid/comments/id
def show
render json: @picture
end
# POST pictures/pid/comments
def create
@picture.comments.create!(comment_params)
render json: @picture.comments, status: :created
# when we submit a form we want to send back the comments of a # picture as the response.this way we can update the DOM
enddef update
@comment.update(comment_params)
head :no_content
end
# DELETE pictures/pid/comments/id
def destroy
@comment.destroy
@picture.comments
end
private
def comment_params
params.permit(:name, :comment)
end
def set_picture
@picture = Picture.find(params[:picture_id])
end
def set_picture_comment
@comment = @picture.comments.find_by!(id:params[:id]) if @picture
end
end

And now for some manual tests.

Fire up Postman once more.

Image for post
Image for post

You can test all the other routes on your own.

Make a commit at this point.

Now to React

We will start by creating a comments component. We want our comments to go under our captions and before our likes.

To avoid Unique key warnings from react, we will install a package called uuid.

npm i uuid --save

In your Comments component you should have the following. I’ll explain with comments inside the code.

# first we import necessary modules/filesimport React, { Component } from "react";
import API_URL from "./helpers/apiHelper";
import axios from "axios";
import "../styles/Comment.css";
import uuid from "uuid";
# create a class component
export default class Comments extends Component {
constructor(props) {
super(props);
this.state = {
comments: [],
name: "",
comment: ""
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
}
# When the comment component mounts, we should load the comments
componentDidMount() {
# here we desturcutre our props. it's neater to write it like this.
const { pid } = this.props;
# we pass in the pid as a prop. we get the pid from the pictures #component
axios.get(`${API_URL}/pictures/${pid}/comments`).then(response => {
# when we get the result of our call to the server. we load all the # comments into an array
this.setState({
comments: response.data
});
});
}
# this handles when a new comment is created
handleSubmit(e) {
e.preventDefault(); # prevents the page from reloading
const { name, comment } = this.state;
const { pid } = this.props;
const body = {
name,
comment
};
# send the comment to the server
axios.post(`${API_URL}/pictures/${pid}/comments`, body).then(response => {
# get the response which are comments belonging to a pic
this.setState({ comments: response.data });
});
this.setState({ name: "", comment: "" });
} # once the user hits submit, clear the form fields
handleChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
render() {
const { comments } = this.state;
const displayAllComments = comments.map(comment => (
<div className="commentWrapper" key={uuid()}>
<p className="ShowComment">
<strong className="commentName">{comment.name}</strong>
<span className="comment">{comment.comment}</span>
</p>
</div>
));
return (
<div>
<div className="view">View all comments</div>
{displayAllComments}
<form onSubmit={this.handleSubmit} className="commentForm">
<input
name="name"
id="name"
type="text"
value={this.state.name}
onChange={this.handleChange}
placeholder="Your name: "
className="commentInput"
></input>
<input
name="comment"
id="comment"
type="text"
value={this.state.comment}
onChange={this.handleChange}
placeholder="Your comment: "
className="commentInput"
></input>
<div>
{" "}
<small>hit enter to add comment</small>
</div>
<button type="submit" className="submitComment"></button>
</form>
</div>
);
}
}

Then head back to our pictures component as this is where we would get the pid prop from in order to grab the comments belonging to a picture.

const displayAllPictures = pictures.map(picture => (
<div key={uuid()}>
<PictureCard
key={picture.id}
uName={picture.created_by.toLowerCase()}
displayPicture={picture.img_link}
picture={picture.img_link}
caption={picture.caption}
increaseLikes={this.increaseLikes}
pid={picture.id}
/>
<Comments pid={picture.id} />
<Likes dateCreated={picture.created_at} likes={picture.likes} />
</div>
));

Let’s style our comments. This should be our output after we are done styling.

Image for post
Image for post
.ShowComment {
margin-left: 1.1em;
margin-top: 0.2em;
}
.comment {
margin-left: 0.5em;
}
.view {
margin-top: -0.8em;
margin-left: 1em;
color: grey;
}
.commentName {
margin-top: -2.5em;
}
.commentForm {
margin-left: 1em;
}
.commentInput {
border: none;
border-bottom: 1px solid black;
margin-bottom: 0.5em;
}
.submitComment {
visibility: hidden;
}

Now, our comments should render each time the user hits enter.

Finally, we need a way for the user to delete comments on the click of a button. First import FontAwesome like we did in the Pictures component. Then import the faTrashAlt icon. Use flexbox to style your comment and icon so it looks aligned.

Now add an onClick handler to the icon.

<FontAwesomeIcon icon={faTrashAlt} onClick={this.handleDelete} />

Make sure to bind this handler in your constructor. Now let’s define our handleDelete method. Binding the handleDelete method means that we have to use a currentTarget method to get the id of a specific comment.see below.

handleDelete(e) {
const { pid } = this.props;
const cid = e.currentTarget.getAttribute("cid");
axios.delete(`${API_URL}/pictures/${pid}/comments/${cid}`).then(
this.setState({
comments: response.data,
notification: "Comment deleted"
})
);
setTimeout(() => {
this.setState({
notification: ""
});
}, 3000);
}

I’m also creating a notification state that gets updated each time a note is deleted. After 3s, the notification messaged is removed.

When a comment is deleted, we instructed our API server to send back the comments belonging to a particular picture. We use this to re-render the DOM by setting the state to those comments. Remember that in react, each time we call setState, the DOM is re-rendered.

Written by

Full-stack Engineer. Rails/Node.js/React. I love plantain chips! Reach me at Adebola.dev.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store