Twitter Clone Part 3: Adding Tweet Reactions and Showing Notifications
Tags: javascript, webdev, react, clone
Published: 2022-06-29 14:41:55

![](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/5576ea91d7ba3114218457100189aaf8/twitter-clone-part-3.png?auto=compress%2Cformat&ixlib=php-3.3.0)

In this article, the third part of the [Build a Twitter Clone](https://dev.to/dillionmegida/series/18659) series, you will add support for tweet reactions (likes and comments), threads, and a notifications page.

[Part 1](https://dev.to/dillionmegida/twitter-clone-part-1-connecting-users-to-stream-feeds-and-creating-a-tweet-13c2) focuses on creating the Twitter layout, authenticating users with Stream, adding the create tweet feature, and displaying the home page [activity feeds](https://getstream.io/activity-feeds/). [Part 2](https://dev.to/dillionmegida/twitter-clone-part-2-creating-a-profile-page-and-following-users-62m) focuses on creating a profile page for users and adding the follow-users feature. Please check out these parts, if you haven't, before continuing with this part.

## Add Tweet Reactions

From the previous steps, I have walked you through building the Twitter layout and the **TweetBlock** component:

![block component preview](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/bdf2833192cac48405131ae3a02c05a7/1.-Preview-of-Tweet-Block-component.png?auto=compress%2Cformat&ixlib=php-3.3.0)

This component shows four actions: comment, retweet, like, and share. For the scope of this tutorial, we will only focus on the comment and like actions which are currently not functional. So, let's make them functional.

### Add a Like Reaction

You will create a custom hook for the like reaction functionality to easily manage it. In Part 1, we concluded with **src/components/Tweet/TweetBlock.js** having an `onToggleLike` function in the `TweetBlock` component, which currently does nothing:

```js
const onToggleLike = () => {
  // toggle like reaction
}
```

To make this function work, firstly, let's create the hook. Create a new file **src/hooks/useLike.js** with the following code:

```js
import { useFeedContext } from 'react-activity-feed'

export default function useLike() {
  const feed = useFeedContext()

  const toggleLike = async (activity, hasLikedTweet) => {
    await feed.onToggleReaction('like', activity)
  }

  return { toggleLike }
}
```

The` feed` object from the `useFeedContext` hook has different methods that can be applied to the activities in the feed the `TweetBlock` is used. This feed can be the **timeline** feed for the homepage or the **user** feed for the profile page.

The `toggleLike` function from the hook receives two arguments: the `activity` to be liked/unliked and a `hasLikedTweet` boolean, which is true if the logged-in user has liked the tweet already. You will use the `hasLikedTweet` argument later when you add notifications.

The `onToggleReaction` method on the `feed` object takes a type of reaction (in this case, **like**) and the activity it should be applied to (the current activity the `TweetBlock` component is used for), and it toggles between liking and unliking for a logged-in user.

To add the like reaction functionality, import this hook to the **TweetBlock** component:

```js
// other imports
import useLike from '../../hooks/useLike'
```

Then update the `onToggleLike` function to this:

```js
const onToggleLike = async () => {
  await toggleLike(activity, hasLikedTweet)
}
```

To test this, go to a tweet in your application either made by the logged-in user or a different user and click on the heart icon. You should have this when you click:

![Twitter like reaction](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/5bd4e891e60138f4bdecd46fbe405f39/2.-Tweet-like-reaction.png?auto=compress%2Cformat&ixlib=php-3.3.0)

The toggle happens when you click it again.

In Part 1, we applied styles for the heart icon to be red when clicked, just in case you're wondering 😅.

You can also test this by logging in with a different user and liking the same tweet. You will see the like count incremented:

![Twitter like reaction other user](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/47ad990d7b604a5489f3439a88731c23/3.-Tweet-like-reaction-with-another-user.png?auto=compress%2Cformat&ixlib=php-3.3.0)

### Add a Comment Reaction

The current state of the comment functionality is that when a user clicks the comment icon on a tweet block, the comment dialog shows, and the user can type a comment, but on submitting, nothing happens. In previous parts, we concluded with **src/components/Tweet/TweetBlock.js** having the `CommentDialog` component attached to an `onPostComment` function that does nothing:

```js
const onPostComment = async (text) => {
  // create comment
}
```

To add the comment reaction, we will make this a custom hook. This functionality will be used in the **TweetBlock** component and the **Thread** component (for when a tweet is expanded to show comments).

Create a new file **src/hooks/useComment.js** with the following code:

```js
import { useFeedContext } from 'react-activity-feed'

export default function useComment() {
  const feed = useFeedContext()

  const createComment = async (text, activity) => {
    await feed.onAddReaction('comment', activity, {
      text,
    })
  }

  return {
    createComment,
  }
}
```

With the `onAddReaction` method of the `feed` object, you can add the comment reaction to an activity and pass the comment text.

To use this hook in **src/components/Tweet/TweetBlock.js**, first import it:

```js
// other imports
import useComment from '../../hooks/useComment'
```

Then, get the `createComment` function in the `TweetBlock` component:

```js
const { createComment } = useComment()
```

And finally, update the `onPostComment` function to this:

```js
const onPostComment = async (text) => {
  await createComment(text, activity)
}
```

With this addition, when you enter a comment, you will see the comment reactions incremented.

So far, we have added the like and comment reactions, but we haven't added threads yet. A thread view will show a tweet expanded, showing the comments in a tweet. So, let's add that next.

## Add a Tweet Thread page

The thread page shows a single tweet, the tweet action buttons, a comment form, and the comments made on the tweet:

![Twitter thread page](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/0d5ee6531339d8448988408bd3aff4d9/4.-Tweet-Thread-Page.png?auto=compress%2Cformat&ixlib=php-3.3.0)

This thread view is broken into sections, so we'll build it section by section.

### Create the ThreadHeader Component

The **ThreadHeader** component shows the back button and the tweet text.

Create a new file **src/components/Thread/ThreadHeader.js**, and paste the following:

```js
import { useNavigate } from 'react-router-dom'
import styled from 'styled-components'

import ArrowLeft from '../Icons/ArrowLeft'

const Header = styled.header`
  display: flex;
  align-items: center;
  padding: 15px;

  button {
    width: 25px;
    height: 20px;
    margin-right: 40px;
  }

  span {
    font-size: 20px;
    color: white;
    font-weight: bold;
  }
`

export default function ThreadHeader() {
  const navigate = useNavigate()

  const navigateBack = () => {
    navigate(-1)
  }

  return (
    <Header>
      <button onClick={navigateBack}>
        <ArrowLeft size={20} color="white" />
      </button>
      <span>Tweet</span>
    </Header>
  )
}
```

Using `useNavigate` from `react-router-dom`, you can navigate the user to the previous page they were on in the history session.

### Create the TweetContent Component

This component shows the tweet information, the tweet action buttons, a tweet form to add a comment, and tweet blocks for comments.

The tweet blocks in this component are a little different from the normal tweet blocks we created in Part 1. As you will notice, this block does not have reactions. To avoid so much conditional rendering going on in the **TweetBlock** component, you will create another tweet block component--**TweetCommentBlock**.

#### Create a TweetCommentBlock Component

Create a new file **src/components/Thread/TweetCommentBlock.js**. Start with imports and styles:

```js
import styled from 'styled-components'

import { formatStringWithLink } from '../../utils/string'
import More from '../Icons/More'
import TweetActorName from '../Tweet/TweetActorName'

const Block = styled.div`
  display: flex;
  border-bottom: 1px solid #333;
  padding: 15px 0;

  .user-image {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    overflow: hidden;
    margin-right: 15px;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }

  .comment-tweet {
    flex: 1;
    .link {
      display: block;
      padding-bottom: 5px;
      text-decoration: none;
    }

    &__text {
      color: white;
      font-size: 15px;
      line-height: 20px;
      margin-top: 3px;

      &--link {
        color: var(--theme-color);
        text-decoration: none;
      }
    }
  }

  .more {
    width: 30px;
    height: 20px;
    display: flex;
    opacity: 0.6;
  }
`
```

And for the component:

```js
export default function TweetCommentBlock({ comment }) {
  const { user, data: tweetComment } = comment

  return (
    <Block to="/">
      <div className="user-image">
        <img src={user.data.image} alt="" />
      </div>
      <div className="comment-tweet">
        <div>
          <TweetActorName
            name={user.data.name}
            id={user.id}
            time={comment.created_at}
          />
          <div className="tweet__details">
            <p
              className="comment-tweet__text"
              dangerouslySetInnerHTML={{
                __html: formatStringWithLink(
                  tweetComment.text,
                  'tweet__text--link'
                ).replace(/\n/g, '<br/>'),
              }}
            />
          </div>
        </div>
      </div>
      <button className="more">
        <More size={18} color="white" />
      </button>
    </Block>
  )
}
```

The `TweetCommentBlock` receives the `comment` prop, a comment activity object. From the `comment` object, you can get the `user` and the `data` object (which you have assigned to the `tweetComment` variable).

#### Composing the TweetContent Component

Create a new file **src/components/Thread/TweetContent.js**. Add the imports for the component:

```js
import { format } from 'date-fns'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
import { useState } from 'react'

import { formatStringWithLink } from '../../utils/string'
import BarChart from '../Icons/BarChart'
import Comment from '../Icons/Comment'
import Retweet from '../Icons/Retweet'
import Heart from '../Icons/Heart'
import Upload from '../Icons/Upload'
import TweetForm from '../Tweet/TweetForm'
import TweetCommentBlock from './TweetCommentBlock'
import CommentDialog from '../Tweet/CommentDialog'
import More from '../Icons/More'
import useComment from '../../hooks/useComment'
import useLike from '../../hooks/useLike'
```

There are many icons here for the actions for the tweet. Also, you will use the `useComment` hook here for the comment form.

Next, the styles:

```js
const Container = styled.div`
  padding: 10px 15px;

  .user {
    display: flex;
    text-decoration: none;

    &__image {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      overflow: hidden;
      margin-right: 15px;

      img {
        width: 100%;
        height: 100%;
      }
    }

    &__name {
      &--name {
        color: white;
        font-weight: bold;
      }
      &--id {
        color: #52575b;
        font-size: 14px;
      }
    }

    &__option {
      margin-left: auto;
    }
  }

  .tweet {
    margin-top: 20px;

    a {
      text-decoration: none;
      color: var(--theme-color);
    }

    &__text {
      color: white;
      font-size: 20px;
    }

    &__time,
    &__analytics,
    &__reactions,
    &__reactors {
      height: 50px;
      display: flex;
      align-items: center;
      border-bottom: 1px solid #555;
      font-size: 15px;
      color: #888;
    }

    &__time {
      &--date {
        margin-left: 12px;
        position: relative;

        &::after {
          position: absolute;
          content: '';
          width: 2px;
          height: 2px;
          background-color: #777;
          border-radius: 50%;
          top: 0;
          bottom: 0;
          left: -7px;
          margin: auto 0;
        }
      }
    }

    &__analytics {
      &__text {
        margin-left: 7px;
      }
    }

    &__reactions {
      &__likes {
        display: flex;

        .reaction-count {
          color: white;
          font-weight: bold;
        }

        .reaction-label {
          margin-left: 4px;
        }
      }
    }

    &__reactors {
      justify-content: space-between;
      padding: 0 50px;
    }
  }

  .write-reply {
    align-items: center;
    padding: 15px 0;
    border-bottom: 1px solid #555;
  }
`
```

Next, the component:

```js
export default function TweetContent({ activity }) {
  const feed = useFeedContext()
  const { client } = useStreamContext()

  const { createComment } = useComment()
  const { toggleLike } = useLike()

  const time = format(new Date(activity.time), 'p')
  const date = format(new Date(activity.time), 'PP')

  const tweet = activity.object.data
  const tweetActor = activity.actor.data

  const [commentDialogOpened, setCommentDialogOpened] = useState(false)

  let hasLikedTweet = false

  if (activity?.own_reactions?.like) {
    const myReaction = activity.own_reactions.like.find(
      (l) => l.user.id === client.userId
    )
    hasLikedTweet = Boolean(myReaction)
  }

  const onToggleLike = async () => {
    await toggleLike(activity, hasLikedTweet)
    feed.refresh()
  }

  const reactors = [
    {
      id: 'comment',
      Icon: Comment,
      onClick: () => setCommentDialogOpened(true),
    },
    { id: 'retweet', Icon: Retweet },
    {
      id: 'heart',
      Icon: Heart,
      onClick: onToggleLike,
    },
    { id: 'upload', Icon: Upload },
  ]

  const onPostComment = async (text) => {
    await createComment(text, activity)

    feed.refresh()
  }
}
```

Just like I showed you in Part 1, the `hasLikedTweet` variable is initialized and updated to hold a boolean value if the logged-in user has liked this tweet or not.

Similar to the like reaction functionality you created earlier, the `onToggleLike` function here uses the `onToggleReaction` method on the `feed` object. Also, the `refresh` method on the `feed` object is used to refresh the feed. This part is relevant because, unlike the `FlatFeed` component, which automatically refreshes upon reactions, the `Feed` component, which you will soon use, does not.

Also, the `onPostComment` function uses the `createComment` function from the `useComment` hook and refreshes the feed after a successful comment.

Next, the UI:

```js
export default function TweetContent() {
  //

  return (
    <>
      {commentDialogOpened && (
        <CommentDialog
          activity={activity}
          onPostComment={onPostComment}
          onClickOutside={() => setCommentDialogOpened(false)}
        />
      )}
      <Container>
        <Link to={`/${tweetActor.id}`} className="user">
          <div className="user__image">
            <img src={tweetActor.image} alt="" />
          </div>
          <div className="user__name">
            <span className="user__name--name">{tweetActor.name}</span>
            <span className="user__name--id">@{tweetActor.id}</span>
          </div>
          <div className="user__option">
            <More color="#777" size={20} />
          </div>
        </Link>
        <div className="tweet">
          <p
            className="tweet__text"
            dangerouslySetInnerHTML={{
              __html: formatStringWithLink(
                tweet.text,
                'tweet__text--link'
              ).replace(/\n/g, '<br/>'),
            }}
          />
          <div className="tweet__time">
            <span className="tweet__time--time">{time}</span>
            <span className="tweet__time--date">{date}</span>
          </div>

          <div className="tweet__analytics">
            <BarChart color="#888" />
            <span className="tweet__analytics__text">View Tweet Analytics</span>
          </div>

          <div className="tweet__reactions">
            <div className="tweet__reactions__likes">
              <span className="reaction-count">
                {activity.reaction_counts.like || '0'}
              </span>
              <span className="reaction-label">Likes</span>
            </div>
          </div>

          <div className="tweet__reactors">
            {reactors.map((action, i) => (
              <button onClick={action.onClick} key={`reactor-${i}`}>
                <action.Icon
                  color={
                    action.id === 'heart' && hasLikedTweet
                      ? 'var(--theme-color)'
                      : '#888'
                  }
                  fill={action.id === 'heart' && hasLikedTweet && true}
                  size={20}
                />
              </button>
            ))}
          </div>
        </div>

        <div className="write-reply">
          <TweetForm
            onSubmit={onPostComment}
            submitText="Reply"
            collapsedOnMount={true}
            placeholder="Tweet your reply"
            replyingTo={tweetActor.id}
          />
        </div>
        {activity.latest_reactions?.comment?.map((comment) => (
          <TweetCommentBlock key={comment.id} comment={comment} />
        ))}
      </Container>
    </>
  )
}
```

There are two ways to make comments in the UI. First, there's the comment form where users can type a comment and submit. The second way is by clicking the comment icon, which opens the `CommentDialog` component for typing a comment.

On the `activity` object, you loop through the `latest_reactions.comment` array to display the comments with the `TweetCommentBlock` component.

### Create the ThreadContent Component

This component is made up of the **ThreadHeader** and **TweetContent** components. Create a new file called **src/components/Thread/ThreadContent.js**. Start with the imports:

```js
import { useEffect, useState } from 'react'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import { useParams } from 'react-router-dom'

import LoadingIndicator from '../LoadingIndicator'
import TweetContent from './TweetContent'
import ThreadHeader from './ThreadHeader'
```

With `useParams`, you will get the `id` of the tweet from the URL. Tweet links exist in this format: **/[actorId]/status/[tweetActivityId]**.

Next, the component:

```js
export default function ThreadContent() {
  const { client } = useStreamContext()
  const { id } = useParams()

  const feed = useFeedContext()

  const [activity, setActivity] = useState(null)

  useEffect(() => {
    if (feed.refreshing || !feed.hasDoneRequest) return

    const activityPaths = feed.feedManager.getActivityPaths(id) || []

    if (activityPaths.length) {
      const targetActivity = feed.feedManager.state.activities
        .getIn([...activityPaths[0]])
        .toJS()

      setActivity(targetActivity)
    }
  }, [feed.refreshing])

  if (!client || !activity) return <LoadingIndicator />

  return (
    <div>
      <ThreadHeader />
      <TweetContent activity={activity} />
    </div>
  )
}
```

`feedManager.getActivityPaths` of the `feed` object returns an array with an **id** for the current tweet link. This line is essential to ensure that the activity exists. If it returns an empty array, then the tweet link does not exist.

`feed.feedManager.state.activities` is an immutable Map (created with [Immutabe.js](https://immutable-js.com/)), so you get the activity object using `getIn` and `toJS` methods.

With the `activity` obtained, you pass it to the `TweetContent` component.

### Create the Thread Page

Create a new file called **src/pages/Thread.js** and paste the following:

```js
import { Feed, useStreamContext } from 'react-activity-feed'
import { useParams } from 'react-router-dom'

import Layout from '../components/Layout'
import ThreadContent from '../components/Thread/ThreadContent'

const FEED_ENRICH_OPTIONS = {
  withRecentReactions: true,
  withOwnReactions: true,
  withReactionCounts: true,
  withOwnChildren: true,
}

export default function Thread() {
  const { user } = useStreamContext()

  const { user_id } = useParams()

  return (
    <Layout>
      <Feed
        feedGroup={user.id === user_id ? 'user' : 'timeline'}
        options={FEED_ENRICH_OPTIONS}
        notify
      >
        <ThreadContent />
      </Feed>
    </Layout>
  )
}
```

For the `feedGroup`, you check if the currently logged-in user made the tweet, of which you use "user", and if it's a different user, you use "timeline". This is because a tweet exists in one of these feeds, not on both.

The `FEED_ENRICH_OPTIONS` is relevant so you can get the reactions with each activity. Without this, you will have to make a separate API request to get the comments in the `TweetContent` component.

Lastly, you need to create a route for this component. Go to **src/components/App.js**. Import the thread page:

```js
// other imports
import Thread from './pages/Thread'
```

And add a route for this component:

```js
<Route element={<Thread />} path="/:user_id/status/:id" />
```

With all these plugged in correctly, when you click a tweet block, you will find the thread view. This view also shows the comment reactions made to a tweet.

You can make more comments using the comment dialog or the comment form:

![Adding comments](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/dd68979ad9b6354445b27b1f93bcca8f/5.-Adding-comments-with-thread-comment-form.png?auto=compress%2Cformat&ixlib=php-3.3.0)

![Thread showing comments](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/e30a8ebeced3dba4e1e20de045678a85/6.-Thread-showing-comments.png?auto=compress%2Cformat&ixlib=php-3.3.0)

## Add the Notifications Page

The notifications page will show new follows, likes, and comment notifications:

![Notifications page](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/fb9361c9c4fc5544edf80e2959a93d16/7.-Notifications-page.png?auto=compress%2Cformat&ixlib=php-3.3.0)

The idea with the notifications implementation is to create activities in the notification feed (created in Part 1 when creating [feed groups](https://getstream.io/activity-feeds/docs/node/creating_feeds/) when actions occur). This implies that when you trigger a "like" action, you create an activity in the notification feed with the "like" verb and a reference to the tweet you liked. Similarly, you'll do the same for comments and follow actions.

Before creating a Notifications page, let's start by creating these activities upon these actions we want notifications for.

### Create a useNotification hook

Since notifications will be used for different things, making the functionality a hook would be easier to manage. Create a new file **src/hooks/useNotification.js** with the following code:

```js
import { useStreamContext } from 'react-activity-feed'

export default function useNotification() {
  const { client } = useStreamContext()

  const createNotification = async (userId, verb, data, reference = {}) => {
    const userNotificationFeed = client.feed('notification', userId)

    const newActivity = {
      verb,
      object: reference,
      ...data,
    }

    await userNotificationFeed.addActivity(newActivity)
  }

  return { createNotification }
}
```

The returned `createNotification` function from the hook receives four arguments:

- `userId`: **id** of the user you want to add the notification for
- `verb`: the label for the activity
- `data`: for other properties to add to the activity, for example, the text of a comment
- `reference`: this is optional, but it can be used for referencing a collection, like a tweet, for example

### Create Notifications on Reactions and Follows

In this section, you will use this hook on reactions and follow actions.

#### Create Notifications on Like Reactions

Go to **src/hooks/useLike.js** to add the hook. First, import the hook:

```js
// other imports
import useNotification from './useNotification'
import { useStreamContext } from 'react-activity-feed'
```

You will need the `user` object from the `useStreamContext` hook, as you will see soon.

Import the `createNotification` function and the `user` object:

```js
// ...
const { createNotification } = useNotification()
const { user } = useStreamContext()
```

Then, update the `toggleLike` function to create a notification on liking a tweet:

```js
const toggleLike = async (activity, hasLikedTweet) => {
  const actor = activity.actor

  await feed.onToggleReaction('like', activity)

  if (!hasLikedTweet && actor.id !== user.id) {
    // then it is not the logged in user liking their own tweet
    createNotification(actor.id, 'like', {}, `SO:tweet:${activity.object.id}`)
  }
}
```

The `toggleLike` function first checks if the tweet has not been liked and the actor of the tweet is not the same as the logged-in user. This check is necessary to ensure that the user does not get a notification upon liking their tweet.

In the last argument, the reference passed to the `createNotification` function refers to the tweet collection.

When you like a tweet, a new activity is added to the notification feed. You can try this by going to a different user's account and liking one of **@getstream_io**'s tweets. On the Feeds Explorer on your dashboard, you will see the **notification:getstream_io** created:

![Feeds Explorer new notifications](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/ede78122df002ac4cd538519820e9eca/8.-Feeds-Explorer-showing-new-notification-feed.png?auto=compress%2Cformat&ixlib=php-3.3.0)

And when you browse the activities in this feed, you will find the new like activity you created:

![Feeds explorer like activity notifications feed](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/7ee082a81aaddab4c532f759015fadaa/9.-Feeds-explorer-showing-like-activitiy-in-notifications-feed.png?auto=compress%2Cformat&ixlib=php-3.3.0)

Because you created a **notification feed group** ([in Part 1](https://getstream.io/activity-feeds/docs/react/flat_feeds/?language=javascript#notification-feeds)), you can see the `is_read` and `is_seen` property. Also, the activities are grouped if they are similar.

#### Create Notifications on Comment Reactions

Similar to what you did in the previous step, go to **src/hooks/useComment.js** and import the required hooks:

```js
import { useStreamContext } from 'react-activity-feed'
import useNotification from './useNotification'
```

Next, get the `createNotification` function and `user` object in the `useComment` hook:

```js
// ...
const { createNotification } = useNotification()
const { user } = useStreamContext()
```

And finally, update the `createComment` function:

```js
const createComment = async (text, activity) => {
  const actor = activity.actor

  await feed.onAddReaction('comment', activity, {
    text,
  })

  if (actor.id !== user.id) {
    // then it is not the logged in user commenting on their own tweet

    createNotification(
      actor.id,
      'comment',
      {
        text,
      },
      `SO:tweet:${activity.object.id}`
    )
  }
}
```

The `createComment` function also ensures that there are no notifications sent if the same actor of tweet comments on the tweet.

You can test this notification by commenting on a tweet and checking your Feed's explorer.
### Create Notifications on Follow Actions

One more notification you want to add is for follow actions. In the **useFollow** hook in **src/hooks/useFollow.js**, import the notification hook:

```js
// other imports
import useNotification from './useNotification'
```

Then, update the `toggleFollow` function to this:

```js
const { createNotification } = useNotification()

const toggleFollow = async () => {
  const action = isFollowing ? 'unfollow' : 'follow'

  if (action === 'follow') {
    await createNotification(userId, 'follow')
  }

  const timelineFeed = client.feed('timeline', client.userId)
  await timelineFeed[action]('user', userId)

  setIsFollowing((isFollowing) => !isFollowing)
}
```

In this function, you check if the action is **follow** and create a follow activity in the notification feed.

You can also test this by following a user and checking your Feeds dashboard.

With these notifications created, now you want to display them.

### Create a NotificationContent Component

This component houses the notification header and the notifications for different actions.

To display the different activities in the notifications feed, you will use the [NotificationFeed](https://getstream.github.io/react-activity-feed/components/notification-feed). This component displays the notifications in groups. But you will provide a custom component to handle this grouping.

#### Creating Grouping Components for Notifications

There are three forms of notifications: like, comment, and follow notifications. The structure of the group is like this:

```js
{
  activities: [...activities created on like action],
  activity_count: NUMBER OF ACTIVITIES,
  actor_count: NUMBER OF ACTORS IN THE ACTIVITIES,
  created_at: ...,
  group: GROUP ID BASED ON VERB AND DATE,
  id: ...,
  is_read: ...,
  is_seen: ...,
  verb: VERB OF GROUPED ACTIVITIES,
}
```

Let's create grouping components for them.

##### Create a LikeNotification Group Component

Create a new file **src/components/Notification/LikeNotification.js**. Add the imports and styles:

```js
import { useStreamContext } from 'react-activity-feed'
import { Link, useNavigate } from 'react-router-dom'
import styled from 'styled-components'

import Heart from '../Icons/Heart'

const Block = styled.button`
  padding: 15px;
  border-bottom: 1px solid #333;
  display: flex;

  a {
    color: white;
  }

  span {
    display: inline-block;
  }

  .right {
    margin-left: 20px;
    flex: 1;
  }

  .liked-actors__images {
    display: flex;

    &__image {
      width: 35px;
      height: 35px;
      border-radius: 50%;
      overflow: hidden;
      margin-right: 10px;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }
  }

  .liked-actors__text {
    margin-top: 10px;
    color: white;
    font-size: 15px;

    .liked-actor__name {
      font-weight: bold;

      &:hover {
        text-decoration: underline;
      }
    }
  }

  .tweet-text {
    display: block;
    color: #888;
    margin-top: 10px;
  }
`
```

With the `useNavigate` hook, you will navigate to the tweet that was liked when a user clicks on the notification.

Next, for the component:

```js
export default function LikeNotification({ likedActivities }) {
  const likedGroup = {}
  const navigate = useNavigate()

  const { user } = useStreamContext()

  likedActivities.forEach((act) => {
    if (act.object.id in likedGroup) {
      likedGroup[act.object.id].push(act)
    } else likedGroup[act.object.id] = [act]
  })
}
```

This component receives the `activities` array from the **like** group.

You create a `likedGroup` object that groups activities by the tweet they were made on. The grouping from the notification feeds contains different like activities on tweets.

The next step is to loop over the `likedGroup` to display the like notifications:

```js
export default function LikeNotification({ likedActivities }) {
  // ...

  return (
    <>
      {Object.keys(likedGroup).map((groupKey) => {
        const activities = likedGroup[groupKey]

        const lastActivity = activities[0]

        const tweetLink = `/${user.id}/status/${lastActivity.object.id}`

        return (
          <Block
            className="active"
            onClick={() => navigate(tweetLink)}
            key={groupKey}
          >
            <Heart color="var(--theme-color)" size={25} fill={true} />
            <div className="right">
              <div className="liked-actors__images">
                {activities.map((act) => (
                  <Link
                    to={`/${act.actor.id}`}
                    key={act.id}
                    className="liked-actors__images__image"
                  >
                    <img src={act.actor.data.image} alt="" />
                  </Link>
                ))}
              </div>
              <span className="liked-actors__text">
                <Link
                  className="liked-actor__name"
                  to={`/${lastActivity.actor.id}`}
                >
                  {lastActivity.actor.data.name}
                </Link>{' '}
                <span to={tweetLink}>
                  {activities.length > 1 &&
                    `and ${activities.length - 1} others`}{' '}
                  liked your Tweet
                </span>
              </span>

              <p className="tweet-text">{lastActivity.object.data.text}</p>
            </div>
          </Block>
        )
      })}
    </>
  )
}
```

You loop over each tweet in the `likedGroup` and also loop over the like activities in the tweet to display the author's information.

##### Create a CommentNotification Group Component

Create a new file **src/components/Notification/CommentNotification.js**. Add the imports and styles:

```js
import { Link, useNavigate } from 'react-router-dom'
import { useStreamContext } from 'react-activity-feed'
import styled from 'styled-components'

import { generateTweetLink } from '../../utils/links'
import TweetActorName from '../Tweet/TweetActorName'

const Block = styled.button`
  padding: 15px;
  border-bottom: 1px solid #333;
  display: flex;

  a {
    color: white;
  }

  .user__image {
    width: 35px;
    height: 35px;
    overflow: hidden;
    border-radius: 50%;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  }

  .user__details {
    margin-left: 20px;
    flex: 1;
  }

  .user__reply-to {
    color: #555;
    font-size: 15px;
    margin-top: 3px;

    a {
      color: var(--theme-color);
      &:hover {
        text-decoration: underline;
      }
    }
  }

  .user__text {
    display: block;
    color: white;
    margin-top: 10px;
  }
`
```

Next, the component:

```js
export default function CommentNotification({ commentActivities }) {
  const navigate = useNavigate()
  const { user } = useStreamContext()

  return (
    <>
      {commentActivities.map((cAct) => {
        const actor = cAct.actor

        const tweetLink = generateTweetLink(cAct.replyTo, cAct.object.id)

        return (
          <Block key={cAct.id} onClick={() => navigate(tweetLink)}>
            <Link to={`/${actor.id}`} className="user__image">
              <img src={actor.data.image} alt="" />
            </Link>
            <div className="user__details">
              <TweetActorName
                id={actor.id}
                name={actor.data.name}
                time={cAct.time}
              />
              <span className="user__reply-to">
                Replying to <Link to={`/${user.id}`}>@{user.id}</Link>
                <p className="user__text">{cAct.text}</p>
              </span>
            </div>
          </Block>
        )
      })}
    </>
  )
}
```

This component receives the `commentActivities` prop, which is the `activities` array from the **comment** group. In this component, you loop through the comments and display the user information and the comment text.

##### Create a FollowNotification Group Component

Create a new file **src/components/Notification/FollowNotification.js**. Add the imports and styles:

```js
import { Link } from 'react-router-dom'
import styled from 'styled-components'

import User from '../Icons/User'

const Block = styled.div`
  padding: 15px;
  border-bottom: 1px solid #333;
  display: flex;

  a {
    color: white;
  }

  .right {
    margin-left: 20px;
    flex: 1;
  }

  .actors__images {
    display: flex;

    &__image {
      width: 35px;
      height: 35px;
      border-radius: 50%;
      overflow: hidden;
      margin-right: 10px;

      img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }
    }
  }

  .actors__text {
    margin-top: 10px;
    color: white;
    font-size: 15px;

    span {
      display: inline-block;
    }

    .actors__name {
      font-weight: bold;

      &:hover {
        text-decoration: underline;
      }
    }
  }
`
```

Next, the component:

```js
export default function FollowNotification({ followActivities }) {
  const firstActivity = followActivities[0]

  return (
    <Block>
      <User color="#1c9bef" size={25} />
      <div className="right">
        <div className="actors__images">
          {followActivities.map((follow) => {
            return (
              <Link
                to={`/${follow.actor.id}`}
                className="actors__images__image"
                key={follow.id}
              >
                <img src={follow.actor.data.image} alt="" />
              </Link>
            )
          })}
        </div>
        <p className="actors__text">
          <Link className="actors__name" to={`/${firstActivity.actor.id}`}>
            {firstActivity.actor.data.name}
          </Link>{' '}
          <span>
            {followActivities.length > 1 &&
              `and ${followActivities.length - 1} others`}{' '}
            followed you
          </span>
        </p>
      </div>
    </Block>
  )
}
```

This component receives the `followActivities` prop, which is the `activities` array of the **follow** group. In this component, you get the first activity from the array so that you can display, "Person A and 5 others followed you".

With these group components created, you can put them together to form a **NotificationGroup** component.

#### Create a NotificationGroup Component

Create a new file **src/components/Notification/NotificationGroup.js** file. Add imports and styles:

```js
import { useEffect, useRef } from 'react'
import { useFeedContext, useStreamContext } from 'react-activity-feed'
import styled from 'styled-components'

import CommentNotification from './CommentNotification'
import FollowNotification from './FollowNotification'
import LikeNotification from './LikeNotification'

const Container = styled.div`
  button {
    width: 100%;
  }
`
```

Next, the component:

```js
export default function NotificationGroup({ activityGroup }) {
  const feed = useFeedContext()
  const notificationContainerRef = useRef()

  const activities = activityGroup.activities

  const { user, client } = useStreamContext()

  useEffect(() => {
    // stop event propagation on links
    if (!notificationContainerRef.current) return

    const anchorTags = notificationContainerRef.current.querySelectorAll('a')

    anchorTags.forEach((element) => {
      element.addEventListener('click', (e) => e.stopPropagation())
    })

    return () =>
      anchorTags.forEach((element) => {
        element.addEventListener('click', (e) => e.stopPropagation())
      })
  }, [])

  useEffect(() => {
    const notifFeed = client.feed('notification', user.id)

    notifFeed.subscribe((data) => {
      if (data.new.length) {
        feed.refresh()
      }
    })

    return () => notifFeed.unsubscribe()
  }, [])
}
```

In the first `useEffect` expression, you stop event propagation on all links in the container ref. The relevance of this is that when you click on a user's name in a like notification block, you don't want the notification block to also navigate to the tweet that was liked.

In the second `useEffect` expression, you subscribe to the notification feed of the logged-in user. On new notifications, you call the `refresh` method on the `feed` object so that the new notifications are displayed.

Finally, for this component, the UI:

```js
export default function NotificationGroup() {
  // ...

  return (
    <Container ref={notificationContainerRef}>
      {activityGroup.verb === 'like' && (
        <LikeNotification likedActivities={activities} />
      )}
      {activityGroup.verb === 'follow' && (
        <FollowNotification followActivities={activities} />
      )}
      {activityGroup.verb === 'comment' && (
        <CommentNotification commentActivities={activities} />
      )}
    </Container>
  )
}
```

In the UI, you check the verb of the group and render the group notification accordingly.

#### Composing the NotificationContent Component

Create a new file **src/components/Notification/NotificationContent.js**. Add the imports and styles:

```js
import classNames from 'classnames'
import { useState } from 'react'
import { NotificationFeed } from 'react-activity-feed'
import styled from 'styled-components'

import NotificationGroup from './NotificationGroup'

const Container = styled.div`
  h1 {
    padding: 15px;
    font-size: 16px;
    color: white;
  }

  .tab-list {
    margin-top: 10px;
    border-bottom: 1px solid #333;
    display: grid;
    grid-template-columns: 1fr 1fr;

    .tab {
      color: #777;
      padding: 0 35px;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-weight: bold;
      font-size: 15px;

      &:hover {
        background-color: #111;
      }

      &__label {
        position: relative;
        padding: 20px 30px;

        &.active {
          color: white;

          &::after {
            content: '';
            height: 3px;
            width: 100%;
            background-color: var(--theme-color);
            border-radius: 40px;
            position: absolute;
            bottom: 0;
            left: 0;
          }
        }
      }
    }
  }
`
```

Next, the component:

```js
const tabList = [
  {
    id: 'all',
    label: 'All',
  },
  {
    id: 'mentions',
    label: 'Mentions',
  },
]

export default function NotificationContent() {
  const [activeTab, setActiveTab] = useState(tabList[0].id)

  return (
    <Container>
      <h1>Notifications</h1>
      <div className="tab-list">
        {tabList.map((tab) => (
          <button
            onClick={() => setActiveTab(tab.id)}
            className="tab"
            key={tab.id}
          >
            <span
              className={classNames(
                'tab__label',
                activeTab === tab.id && 'active'
              )}
            >
              {tab.label}
            </span>
          </button>
        ))}
      </div>
      <NotificationFeed Group={NotificationGroup} />
    </Container>
  )
}
```

Although the tab list is not functional, it's nice to have. In this component, you use the `NotificationFeed` and pass the `NotificationGroup` component to the `Group` prop.

### Creating the Notifications Page

Create a new file **src/pages/Notifications.js** with the following code:

```js
import Layout from '../components/Layout'
import NotificationContent from '../components/Notification/NotificationContent'

export default function Notifications() {
  return (
    <Layout>
      <NotificationContent />
    </Layout>
  )
}
```

And also, add a route in **App.js** for this page:

```js
// other imports

import Notifications from './pages/Notifications'
```

```js
<Route element={<Notifications />} path="/notifications" />
```

### Show a Notification Counter

When a user has unread notifications, you will show the count of those notifications in a badge on the Notifications link:

![Notifications count badge](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/2a1631046f401904a9e9206a72098cff/10.-Notifications-like-showing-notifications-count-badge.png?auto=compress%2Cformat&ixlib=php-3.3.0)

This notification link exists in the **LeftSide** component. Go to **src/components/LeftSide.js** and import `useEffect`:

```js
// other imports
import { useEffect } from 'react'
```

When this component mounts, you will query the notification feed of the logged-in user, get the notifications that haven't been seen (the `is_seen` property will be `false`), and display the count. In the `LeftSide` component, add the following:

```js
export default function LeftSide({ onClickTweet }) {
  // ...other things

  const { client, userData } = useStreamContext()

  useEffect(() => {
    if (!userData || location.pathname === `/notifications`) return

    let notifFeed

    async function init() {
      notifFeed = client.feed('notification', userData.id)
      const notifications = await notifFeed.get()

      const unread = notifications.results.filter(
        (notification) => !notification.is_seen
      )

      setNewNotifications(unread.length)

      notifFeed.subscribe((data) => {
        setNewNotifications(newNotifications + data.new.length)
      })
    }

    init()

    return () => notifFeed?.unsubscribe()
  }, [userData])

  // other things
}
```

When the component mounts, you create an `init` function and evoke it. In this function, you get all the activities in the notification feed; then, you filter out the notifications that have been seen to find the unread ones. Next, you update the `newNotifications` state with the length of the unread array.

Also, you subscribe to the notification feed so that when a new activity is added to the notification feed, you update the `newNotifications` state.

Remember earlier you triggered some notifications on **getstream_io**'s account by liking, commenting on their tweet, and following them. Now when you log into **getstream_io**'s account and click the notifications link on the left sidebar, you will see the notification activities made on their feed like this:

![Notifications page](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/fb9361c9c4fc5544edf80e2959a93d16/7.-Notifications-page.png?auto=compress%2Cformat&ixlib=php-3.3.0)

And there you have it, your Twitter clone!

## Conclusion

There are more features that can be added to this clone project, but we have focused on some functionalities that allow you to understand activity feeds and how Stream feeds provides solutions for feed-based applications.

Find the complete source code of the clone [in this repository](https://github.com/GetStream/react-twitter-clone).

Please give the [react-activity-feed repository](https://github.com/GetStream/react-activity-feed) a star if you enjoyed this tutorial.

As a recap:

- in [Part 1](https://dev.to/dillionmegida/twitter-clone-part-1-connecting-users-to-stream-feeds-and-creating-a-tweet-13c2), we built most of the layout and shared components and also added the create-tweet feature
- in [Part 2](https://dev.to/dillionmegida/twitter-clone-part-2-creating-a-profile-page-and-following-users-62m), we added a profile page for users and also created the follow-user functionality
- in this part, we added support for like and comment reactions and created notifications for each action.

Overall in this [Twitter clone](https://dev.to/dillionmegida/build-a-twitter-clone-with-activity-feeds-and-react-5fa0), you should now understand the concept of:

- activity feeds (tweets or notification activities)
- subscribing to a feed (following a user)

There are many more ways you apply feeds. You can use them in forums (where a user can subscribe to a topic or discussion), e-commerce platforms (where users can follow a product feed and get updated when new related products are added), and social media platforms.

We have [other feeds SDKs](https://getstream.io/activity-feeds/) to allow you to integrate feeds in different languages and platforms. Do check it out.