How to Render Likes on a Page
Imagine this scenario… You work for a social media company and your manager asks you hey can you tackle the like feature on the forum we are building for the new application. Simple enough right? In practice, this was a challenging endeavor, at least for me, that involves significant thought. This common problem in application design was a rewarding experience that really made me practice my React chops. In this blog, I will go over some of my thought process and then how I implemented a liking feature on a forum application.
The Structure of the Forum
The forum application consists of a forum component, a channel component, a message list component, and a message component. The forum component deals with the changing and updates of the channel. The channel component then handles fetching the messages from the back end for the channel and passing the messages down into the message list as follows:
class Channel extends Component {
state = {
messages: []
} componentDidMount() {
fetch(`http://localhost:3000/api/v1/channels/
${this.props.channelID}`), {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.token}`,
}
})
.then(response => response.json())
.then(channel => this.setState({ messages: channel.messages }))
} render() {
return(
<MessageList messages=this.state.messages>
)
}
}
The message list component receives the messages as props from the channel component. The message list component then sends the information from each message to an individual message component to be rendered to the page.
class MessageList extends Component {
showMessageList = () => {
if(this.props.messages){
return this.props.messages.map((message, index) => {
<Message
key={index}
id={message.id}
username={message.user.username}
likes={message.likes.length}
body={message.body}
/>
})
}
} render() {
return (
<>
{this.showMessageList()}
</>
)
}
}
The messages in the backend contain a likes array that holds which users liked the message. The message component then renders this information sent down to the page.
class Message extends Component {
showMessage = () => {
return(
<div className="message">
<h3>
{this.props.username} - Likes: {this.props.likes}{" "}
<LikeFilled />
</h3>
<p>{this.props.body}</p>
</div>
)
}
render() {
return(
<>
{this.showMessage()
</>
})
}
}
LikeFilled is a component that shows an icon that the user can click to increase likes. This shows the base structure of the forum used to increment likes on a message. The next step is to determine if a user already liked a message.
Determining if a user has already liked a message
We can write a function that determines if a user already liked a message. This way we can conditionally render the like button and prevent a duplicate like from being counted and sent to the backend. So, we can write a function called messageLiked in the message list component that checks if a user matches any of the users of likes. I used the context API to pull the user information from a context object.
class MessageList extends Component {
static contextType = userContext; messageLiked = (message) => {
const { user } = this.context;
let liked = false;
if(message.likes.length === 0) {
return liked;
}
message.likes.forEach(like => {
if(like.user_id === user.id) {
liked = true;
}
})
return liked;
} showMessageList = () => {
if(this.props.messages){
return this.props.messages.map((message, index) => {
<Message
key={index}
id={message.id}
username={message.user.username}
likes={message.likes.length}
liked={this.messageLiked(message}
body={message.body}
/>
})
}
}render() {
return (
<>
{this.showMessageList()}
</>
)
}
}
Using liked flag sent down to the message component, we can conditionally render the like button as follows.
class Message extends Component {
showMessage = () => {
return(
<div className="message">
<h3>
{this.props.username} - Likes: {this.props.likes}{" "}
{this.props.liked ? null : <LikeFilled />}
</h3>
<p>{this.props.body}</p>
</div>
)
}
render() {
return(
<>
{this.showMessage()
</>
})
}
}
Now, the user can only update the likes object if the user has not liked the message previously. The next step will be to create an event listener and function to update the likes on the backend.
Sending Likes to the Backend
The next step is to create an incrementALike function that will handle sending the like to the backend on the click of the like button. This will be defined in the channel component and passed down. The function is defined in the channel component so that we can manipulate the messages in the channel’s state in a later step.
class Channel extends Component {
state = {
messages: []
} componentDidMount() {
fetch(`http://localhost:3000/api/v1/channels/
${this.props.channelID}`), {
method: 'GET',
headers: {
'Authorization': `Bearer ${localStorage.token}`,
}
})
.then(response => response.json())
.then(channel => this.setState({ messages: channel.messages }))
} incrementALike = (message_id) => {
let like = {
user_id: user.id,
message_id: message_id
} fetch('http://localhost:3000/api/v1/likes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(like)
})
}; render() {
return(
<MessageList
messages=this.state.messages
incrementALike={this.incrementALike}
/>
)
}
}
The incrementALike function is passed down to the message component through the message list component as a prop.
class MessageList extends Component {
static contextType = userContext; messageLiked = (message) => {
const { user } = this.context;
let liked = false;
if(message.likes.length === 0) {
return liked;
}
message.likes.forEach(like => {
if(like.user_id === user.id) {
liked = true;
}
})
return liked;
} showMessageList = () => {
if(this.props.messages){
return this.props.messages.map((message, index) => {
<Message
key={index}
id={message.id}
username={message.user.username}
likes={message.likes.length}
liked={this.messageLiked(message}
incrementALike={this.props.incrementALike}
body={message.body}
/>
})
}
} render() {
return (
<>
{this.showMessageList()}
</>
)
}
}
In the message component, we need to create a click handler function. This handleClick function will fire the incrementALike function when the like button is clicked and send a like associated with the user and the message to the backend.
class Message extends Component {
handleClick = () => {
this.props.incrementALike(this.props.id)
} showMessage = () => {
return(
<div className="message">
<h3>
{this.props.username} - Likes: {this.props.likes}{" "}
{this.props.liked
? null
: <LikeFilled onClick={this.handleClick}
/>}
</h3>
<p>{this.props.body}</p>
</div>
)
}
render() {
return(
<>
{this.showMessage()
</>
})
}
}
This handles updating likes and persisting likes on refresh, but the page still does not show the update in real time. We want to make it so that the page renders when the user clicks the like button. To do so, we need to update the messages in the state of the channel. Whenever the state is updated, the new messages will be passed down and the forum will be updated in real time.
Updating the Likes on the Front End
To update the likes on the front end, we need to edit the incrementALike function so that the messages in the state of the channel component reflects the changes sent to the backend. This involves creating a new variable for the updated messages, finding the message that was liked, updating the message by adding the like to the likes array, and updating the state with the updated messages.
class Channel extends Component {
state = {
messages: []
} componentDidMount() {
fetch(`http://localhost:3000/api/v1/channels/
${this.props.channelID}`), {
method: 'GET',
headers: {
'Authorization': `B. earer ${localStorage.token}`,
}
})
.then(response => response.json())
.then(channel => this.setState({ messages: channel.messages }))
} incrementALike = (message_id) => {
const { user } = this.context;
let new_messages = this.state.messages
let liked_message = this.state.messages.find(
message => message.id === message_id
)
let liked_message_idx = this.state.messages.findIndex(
message => message.id === message_id
) liked_message.likes.push(like) new_messages[liked_message_idx] = liked_message
this.setState({ messages: new_messages }) let like = {
user_id: user.id,
message_id: message_id
} fetch('http://localhost:3000/api/v1/likes', {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(like)
})
}; render() {
return(
<MessageList
messages=this.state.messages
incrementALike={this.incrementALike}
/>
)
}
}
Conclusion
In conclusion, adding a like feature to a message in a forum is a fairly involved process to make it persist and render on the page when the user clicks on it. But, going through this process with bumps along the way really solidified my understanding of passing down information and where to update state. I hope this post will serve as a guide to anyone looking to implement this functionality.