Twitter Clone Part 1: Connecting Users to Stream Feeds and Creating a Tweet

In this tutorial, you will learn how to implement Stream activity feeds in creating a Twitter clone with the React SDK.

Dillion M.
Dillion M.
Published June 14, 2022
Build a Twitter Clone Part 1 Feature Image

In this article, the first part of the Build a Twitter Clone series, you will be creating a Twitter clone, which connects and authenticates a selected user with your Stream app. For this tutorial, you will make the layout and add support for creating tweets in the clone using Stream activity feeds.

Let us get started.

Developer Setup

To start building the clone, you need to install dependencies from the npm library. To install dependencies, you need to have Node installed on your system. Alternatively, you can use yarn, but I will be using npm in this article.

I used node version 16.13.1 for this tutorial, so I recommend you use the same to code along.

Setting Up your Stream Dashboard

A Stream Feeds App handles all the backend services for managing feeds, such as creating activities, adding reactions to activities, following and unfollowing activities, etc.

Create A Feeds App on Your Stream Dashboard

To create a feeds app on Stream, you need a Stream account. If you do not have one, head over to the register page for a free trial account or register for a Maker Account for free indefinite access to Stream.

To create a Stream app for feeds:

  1. Go to your Stream dashboard after logging in
  2. Select Create App
  3. Enter a name for the app (for this tutorial, I will use streamer, the fancy name for the clone)
  4. Set your Server Locations
  5. Set the Environment to Development
  6. And finally, select Create App.

After creating the app, select Activity Feeds from the sidebar:

Selecting activity feeds from the left sidebar

Here is the overview of the Feeds dashboard:

Overview of feeds dashboard

You have the App ID, API Key, and API Secret. You will need these values later when you want to connect to Stream from your client app.

Create Feed Groups

Currently, there are no feed groups in your app:

Empty feed groups

A feed group is used for grouping similar activities together. For example, in this tutorial, you will have a:

  • "timeline" feed group for activities made by users that a user follows
  • "user" feed group for activities made by a user
  • "notification" feed group for notification activities originating from follow or reaction actions
Adding a feed group

For the timeline and user group, use a flat feed type, and a notification group with a notification feed type.

With Stream set up, you can now create the client application.

Create Your Twitter Clone Application

We will use create-react-app (CRA) to create the React application. On your terminal, run:

bash
1
2
npx create-react-app streamer cd streamer

This command will create a starter React project. Now, you need to install the required dependencies. These dependencies are broken into two categories.

Stream's dependencies

  • getstream: official JavaScript client for Stream Feeds
  • react-activity-feed: built on the getstream library for providing React components to integrate activity feeds into your application.

Other dependencies

  • react-router-dom: for adding routes for different pages in the application. You will use it to add pages for the starter login page, home page, profile page, and notifications page
  • classnames: utility library for dynamically combining classes
  • date-fns: for formatting dates in a readable manner
  • styled-components: for CSS-in-JS styles
  • nanoid: for generating unique IDs. You will use this to generate IDs for tweets

Install the dependencies by running:

bash
1
npm install getstream react-activity-feed react-router-dom classnames date-fns styled-components nanoid

If you come across a dependency resolution error for react and react-activity-feed similar to this:

20. Dependency error

You can add the --force flag to the npm install command. This will ignore the resolution error. The error above occurs because CRA installs the latest versions of react and react-dom (which is version 18, released recently), but Stream’s dependencies haven’t been updated to support React v18 yet. In this project, we won’t be using specific React v18 features.

Folder Structure of the Application

To keep your code organized and so you can follow this tutorial correctly, you should use the following folder structure for this application.

After starting the project with CRA, you should get this:

├── README.md
├── package-lock.json
├── package.json
├── node_modules
├── public
| ├── favicon.ico
| ├── index.html
| ├── logo192.png
| ├── logo512.png
| ├── manifest.json
| └── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
└── setupTests.js

You will need new folders to improve the structure. Create the following folders:

  • src/components: where the components--the building blocks in your application--will be created
  • src/pages: where the page components (profile, notifications, etc.) will be created
  • src/hooks: where the custom hooks you create in this tutorial will live
  • src/utils: where the utilities will live

With these folders created, you should have the following structure:

├── README.md
├── package-lock.json
├── package.json
├── public
| ├── favicon.ico
| ├── index.html
| ├── logo192.png
| ├── logo512.png
| ├── manifest.json
| └── robots.txt
└── src
├── App.css
├── App.js
├── App.test.js
├── components/
├── hooks/
├── index.css
├── index.js
├── logo.svg
├── reportWebVitals.js
├── setupTests.js
├── utils/
└── pages/

Create Starter Page for Selecting Users

The starter page for this application shows different demo users that a user can select from to use Streamer:

Starter page that user will build

Ideally, there should be a login form that sends requests to a backend server, which authenticates the user's credentials with the database. For demonstration purposes, we will stick with demo users.

Add Demo Users

Create a new file called src/users.js and paste the following code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const users = [ { id: 'iamdillion', name: 'Dillion', image: 'https://dillionmegida.com/img/deee.jpg', bio: 'Just here, doing my thing. Developer advocate at @getstream_io', token: 'ENTER TOKEN FOR iamdillion', }, { id: 'getstream_io', name: 'Stream', image: 'https://avatars.githubusercontent.com/u/8597527?s=200&v=4', bio: 'Deploy activity feeds and chat at scale with Stream – an API driven platform powering over a billion end users. Get started at http://getstream.io.', token: 'ENTER TOKEN FOR getstream_io', }, { id: 'jake', name: 'Jake', image: 'https://picsum.photos/300/300', bio: 'Just Jake, nothing much', token: 'ENTER TOKEN FOR jake', }, { id: 'joe', name: 'Joe', image: 'https://picsum.photos/200/200', bio: 'How are you?', token: 'ENTER TOKEN FOR joe', }, { id: 'mike', name: 'Mike', image: 'https://picsum.photos/400/400', bio: 'I am mike here. I do things on #react and #javascript', token: 'ENTER TOKEN FOR mike', }, ] export default users

This is an array of users. Each user object has an id which is a required property to connect the user to Stream feeds. This id will also be used as the Streamer username of each user. Each object also has a name, image, and bio property.

In a live application, the token should also be generated from the backend server using the API Key and Secret Key of your Stream app, but for tutorial purposes, you can manually generate tokens on generator.getstream.io using the user's id and your application's API Key and Secret Key. When you generate a token for a user, replace it in the users.js file.

Stream uses User Tokens to authenticate users--to confirm that users have access to your Stream application.

Create a Storage Utility

Next, create a storage utility in src/utils/storage.js. This utility handles storing and retrieving data from local storage. Add the following code to this file:

js
1
2
3
4
export const saveToStorage = (key, value) => window.localStorage.setItem(key, value) export const getFromStorage = (key) => window.localStorage.getItem(key)

You will use this utility to save the selected user id from the start page. This way, the user will not have to choose a user on every refresh.

Add Global Default Styles

You need to add global default styles for buttons, links, and other elements. Replace the content of src/index.css with the following:

css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
:root { --theme-color: #f91680; --faded-theme-color: #f916803c; } * { box-sizing: border-box; } body { margin: 0; background-color: black; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } code { font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } button { border: none; background: none; cursor: pointer; text-align: left; } button:disabled { cursor: not-allowed; } h1, h2, h3, h4, h5, h6, p { margin: 0; } input, textarea { font-family: inherit; } span { display: block; } a { text-decoration: none; }

The --theme-color variable will be used in many parts of the application.

Create the StartPage Component

Create a new file src/views/StartPage.jsfor the start page, and paste the following. Start from the imports and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import styled from 'styled-components' import users from '../users' import { saveToStorage } from '../utils/storage' const Main = styled.main` background-color: black; display: flex; align-items: center; justify-content: center; width: 100%; height: 100vh; flex-direction: column; h1 { text-align: center; color: white; font-size: 20px; margin-bottom: 20px; } .users { display: flex; align-items: center; justify-content: space-between; width: 300px; margin: 0 auto; &__user { display: flex; flex-direction: column; img { width: 50px; height: 50px; border-radius: 50%; margin-bottom: 5px; } .name { margin: 10px auto; color: white; text-align: center; } } } `

And next, the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function Startpage() { const onClickUser = (id) => { saveToStorage('user', id) window.location.href = '/home' } return ( <Main> <h1>Select a user</h1> <div className="users"> {users.map((u) => ( <button onClick={() => onClickUser(u.id)} className="users__user" key={u.id} > <img src={u.image} alt="" /> <span className="name">{u.name}</span> </button> ))} </div> </Main> ) }

In the StartPage component, you loop through the users on the page, and on clicking a user, you save the user's id to local storage and navigate to the /home path.

Note: For some of the colors in this application, I randomly selected a close color code, but for some notable colors (like a black background, pink highlights, etc.), I used the Eye Dropper Chrome extension to pick the color from the Twitter page.

Next, you have to configure React Router to show the start page on the index path.

Configure Route for the StartPage Component

Replace the content of src/App.js with the following:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' import StartPage from './pages/StartPage' export default function App() { return ( <Router> <Routes> <Route path="/" element={<StartPage />} /> </Routes> </Router> ) }

Start the development server by running the npm run start command on your terminal. On http://localhost:3000, you will get the users on the screen.

When you click on a user on this page, the browser navigates to /home, which should show the home page of the logged-in user.

Create the User Homepage

In this section, you will create a homepage for the user.

Here is what the result of this section will look like:

Home page user will build

Add Icon Components

A lot of icons are used throughout this project. I got the icons from remixicon and made them reusable React components. You can find all the icons in this archived file in the repo. Create a new folder src/components/Icons and save all the icons from the archive there.

All icons have a size and color property that you can use to customize the icon's look. Some icons also have the fill property, which specifies if the icon should be in stroke form or fill form.

Connect a User to Stream Feeds in App.js

The next step is to connect the selected user from the start page to the Feeds App on your Stream dashboard. To connect a user to Stream, you first need to create an instance of your Stream app in your React application. To do this, you use the StreamClient constructor from the getstream library. After creating the instance, then you can connect the user to Stream. And with the StreamApp component from the React SDK, you can provide feed methods and data to other components.

In your App.js file, add the following imports to the existing imports:

js
1
2
3
4
5
import { useEffect, useState } from 'react' import { StreamClient } from 'getstream' import { StreamApp } from 'react-activity-feed' import users from './users' import { getFromStorage } from './utils/storage'

Using getFromStorage, you will get the user's id, and find that user in the users array. If such a user exists, then you connect them to Stream. This approach is our own method of authentication for development 😁

To connect to your feeds application, you need your App ID and API Key. You can get these from your dashboard, as shown in the screenshot below:

Get app id and api key

Assign these values to variables in App.js like this:

js
1
2
const APP_ID = '1183905' const API_KEY = 'mx8gc4kmvpec'

Before the return statement in the App component, add these lines of code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function App() { const userId = getFromStorage('user') const user = users.find((u) => u.id === userId) || users[0] const [client, setClient] = useState(null) useEffect(() => { async function init() { const client = new StreamClient(API_KEY, user.token, APP_ID) await client.user(user.id).getOrCreate({ ...user, token: '' }) setClient(client) } init() }, []) if (!client) return <></> return ( // ... ) }

First, you get the user's id. Next, you find the user from the users array. If the user does not exist, you set the user variable as the first user in the array.

You also keep track of the client state you will use in a second.

When the component mounts, you connect the user to Stream. The component must mount first because connecting a user to Stream creates a WebSocket connection on the browser. The useEffect hook with an empty dependency array runs when the component mounts.

In the useEffect hook, you create the app instance using your API_KEY, the user's token, and your APP_ID. Using the instance, you can define a user by their id, and add the user to the Stream database if they do not exist already using the getOrCreate method. As the name implies, this method retrieves the user's info from the database, and if the user does not exist, it adds the user to the database. You can find the user feeds in your dashboard explorer:

Users in the feeds database

After connecting the user, you update the client state. Now, you can use the client object. In the App component, wrap the elements with the StreamApp component like this:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
function App() { // ... return ( <StreamApp token={user.token} appId={APP_ID} apiKey={API_KEY}> <Router> <Routes> <Route path="/" element={<StartPage />} /> </Routes> </Router> </StreamApp> ) }

The StreamApp component provides feed methods and context data to the children components to trigger feed functionalities.

With the code so far, when you click on a user from the start page, the browser navigates to the /home route, and the selected user is connected to Stream. Now, you will create a layout for the home page.

Create Common Shareable Components

This project has some common components that you will reuse in many other components. Creating these components separately makes the code more manageable.

Create a Loading Indicator Component

Before creating the layouts, you need to create a loading indicator component that you will use in other components. Create a new file src/components/LoadingIndicator. In that file, paste the following:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import styled from 'styled-components' const Container = styled.div` width: 100%; height: 100%; display: flex; justify-content: center; padding-top: 100px; background-color: black; .circle { border: 2px solid #333; border-radius: 50%; position: relative; width: 25px; height: 25px; &::after { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-top: 2px solid var(--theme-color); border-radius: 50%; animation: spin 500ms infinite linear; @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } } } ` export default function LoadingIndicator() { return ( <Container> <div className="circle"></div> </Container> ) }

This creates an infinite rotating circle.

Create a Modal Component

The Modal component serves as a modal dialog for different elements such as the tweet form, comment form, etc.

Modal component user will build

Create a new file src/components/Modal.js and paste the imports and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import classNames from 'classnames' import styled from 'styled-components' import Close from './Icons/Close' const Container = styled.div` position: fixed; z-index: 6; width: 100%; height: 100vh; display: flex; justify-content: center; padding: 30px 0; left: 0; top: 0; .modal { z-index: 2; position: relative; background-color: black; border-radius: 20px; .close-btn { position: relative; left: -10px; } } ` const Backdrop = styled.div` position: absolute; width: 100%; height: 100%; left: 0; top: 0; background-color: rgba(255, 255, 255, 0.2); `

Next, the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
export default function Modal({ className, children, onClickOutside }) { return ( <Container> <Backdrop onClick={() => onClickOutside()} /> <div className={classNames('modal', className)}> <button onClick={onClickOutside} className="close-btn"> <Close color="white" size={24} /> </button> {children} </div> </Container> ) }

On clicking the Backdrop styled component or the close button, the onClickOutside function is called, which is expected to hide the modal conditionally.

Create a ScrollToTop Component

When you navigate to a new page in React using the Link component from react-router-dom, the scroll position would usually retain its position. This component you are about to build will help resolve that by automatically scrolling to the top of the page on every route change.

Create a new file src/components/ScrollToTop.js with the following code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useEffect } from 'react' import { useLocation } from 'react-router' const ScrollToTop = (props) => { const location = useLocation() useEffect(() => { window.scrollTo(0, 0) }, [location]) return <>{props.children}</> } export default ScrollToTop

When the location object changes, the useEffect hook triggers the scroll to top expression.

Next, you will add this component in App.js.

js
1
2
// other imports import ScrollToTop from './components/ScrollToTop'
js
1
2
3
4
5
6
7
8
9
10
11
export default function App() { // ... return ( <StreamApp token={user.token} appId={APP_ID} apiKey={API_KEY}> <Router> <ScrollToTop /> // routes </Router> </StreamApp> ) }

Create FollowBtn Component

Follow button component

The follow button is used for following and unfollowing users. Create a new file src/components/FollowBtn. Add the imports and the styles:

js
1
2
3
import classNames from 'classnames' import styled from 'styled-components' import { useState } from 'react'

Next, the UI of the button:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default function FollowBtn({ userId }) { const [following, setFollowing] = useState(false) return ( <Container> <button className={classNames(following ? 'following' : 'not-following')} onClick={() => setFollowing(!following)} > {following ? ( <div className="follow-text"> <span className="follow-text__following">Following</span> <span className="follow-text__unfollow">Unfollow</span> </div> ) : ( 'Follow' )} </button> </Container> ) }

This component is not fully functional as that is not the scope of this part. Part 3 adds more to the code. For now, the component receives the userId prop (which it doesn't use yet) and toggles the following state when clicked.

Create a TweetForm component

Tweet form with some text and buttons

The TweetForm component is a shareable form component with the tweet input and a submit button. Create a new file src/components/Tweet/TweetForm.js. Import some libraries:

js
1
2
3
4
5
6
7
8
9
10
11
12
import classNames from 'classnames' import { useEffect, useRef, useState } from 'react' import { useStreamContext } from 'react-activity-feed' import styled from 'styled-components' import Calendar from '../Icons/Calendar' import Emoji from '../Icons/Emoji' import Gif from '../Icons/Gif' import Image from '../Icons/Image' import Location from '../Icons/Location' import Poll from '../Icons/Poll' import ProgressRing from '../Icons/ProgressRing'

The ProgressRing component indicates the text length and shows when the text exceeds the maximum available length.

Next, the styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
const Container = styled.div` width: 100%; .reply-to { font-size: 14px; color: #888; display: flex; margin-left: 55px; margin-bottom: 10px; &--name { margin-left: 4px; color: var(--theme-color); } } ` const Form = styled.form` width: 100%; display: flex; align-items: ${({ inline }) => (inline ? 'center' : 'initial')}; .user { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; margin-right: 15px; img { width: 100%; height: 100%; object-fit: cover; } } .input-section { width: 100%; display: flex; flex: 1; flex-direction: ${({ inline }) => (inline ? 'row' : 'column')}; align-items: ${({ inline }) => (inline ? 'center' : 'initial')}; height: ${({ inline, minHeight }) => (inline ? '40px' : minHeight)}; textarea { padding-top: 10px; background: none; border: none; padding-bottom: 0; font-size: 18px; width: 100%; flex: 1; resize: none; outline: none; color: white; } .actions { margin-top: ${({ inline }) => (inline ? '0' : 'auto')}; display: flex; height: 50px; align-items: center; button { &:disabled { opacity: 0.5; } } .right { margin-left: auto; display: flex; align-items: center; } .tweet-length { position: relative; svg { position: relative; top: 2px; } &__text { position: absolute; color: #888; font-size: 14px; top: 0; bottom: 0; left: 0; right: 0; margin: auto; height: max-content; width: max-content; &.red { color: red; } } } .divider { height: 30px; width: 2px; border: none; background-color: #444; margin: 0 18px; } .submit-btn { background-color: var(--theme-color); padding: 10px 20px; color: white; border-radius: 30px; margin-left: auto; font-weight: bold; font-size: 16px; &:disabled { opacity: 0.6; } } } } `

The action buttons, though non-functional:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const actions = [ { id: 'image', Icon: Image, alt: 'Image', }, { id: 'gif', Icon: Gif, alt: 'GIF', }, { id: 'poll', Icon: Poll, alt: 'Poll', }, { id: 'emoji', Icon: Emoji, alt: 'Emoji', }, { id: 'schedule', Icon: Calendar, alt: 'Schedule', }, { id: 'location', Icon: Location, alt: 'Location', }, ]

And for the component, paste this:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export default function TweetForm({ submitText = 'Tweet', onSubmit, className, placeholder, collapsedOnMount = false, minHeight = 120, shouldFocus = false, replyingTo = null, }) { const inputRef = useRef(null) const { client } = useStreamContext() const [expanded, setExpanded] = useState(!collapsedOnMount) const [text, setText] = useState('') useEffect(() => { if (shouldFocus && inputRef.current) inputRef.current.focus() }, []) const user = client.currentUser.data const MAX_CHARS = 280 const percentage = text.length >= MAX_CHARS ? 100 : (text.length / MAX_CHARS) * 100 const submit = async (e) => { e.preventDefault() if (exceededMax) return alert('Tweet cannot exceed ' + MAX_CHARS + ' characters') await onSubmit(text) setText('') } const onClick = () => { setExpanded(true) } const isInputEmpty = !Boolean(text) const charsLeft = MAX_CHARS - text.length const maxAlmostReached = charsLeft <= 20 const exceededMax = charsLeft < 0 const isReplying = Boolean(replyingTo) }

The component receives eight props:

  • submitText: The text on the submit button, which by default is "Tweet"
  • onSubmit: The function called when the submit button is called. This function will be called with the text argument from the input
  • className: For custom class names passed to this component
  • placeholder: Placeholder for the input
  • collapsedOnMount: A boolean to specify if the form is collapsed on mount.
  • minHeight: For the minimum height of the form
  • shouldFocus: A boolean to specify if the input should be focused on mount
  • replyingTo: If the form is a reply to a user, then the user's id will be passed here.

The percentage variable calculates how many characters the user has typed. This value works with the ProgressRing component to indicate how much has been typed and how many characters are left based on the maximum amount.

When the form is submitted, and the input exceeds the maximum length, it throws an alert warning.

Next, the UI of the form:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
export default function TweetForm() { // return ( <Container> {isReplying && expanded && ( <span className="reply-to"> Replying to <span className="reply-to--name">@{replyingTo}</span> </span> )} <Form minHeight={minHeight + 'px'} inline={!expanded} className={className} onSubmit={submit} > <div className="user"> <img src={user.image} alt="" /> </div> <div className="input-section"> <textarea ref={inputRef} onChange={(e) => setText(e.target.value)} placeholder={placeholder} value={text} onClick={onClick} /> <div className="actions"> {expanded && actions.map((action) => { return ( <button type="button" disabled={action.id === 'location' && 'disabled'} key={action.id} > <action.Icon size={19} color="var(--theme-color)" /> </button> ) })} <div className="right"> {!isInputEmpty && ( <div className="tweet-length"> <ProgressRing stroke={2.2} color={ exceededMax ? 'red' : maxAlmostReached ? '#ffd400' : 'var(--theme-color)' } radius={maxAlmostReached ? 19 : 14} progress={percentage} /> {maxAlmostReached && ( <span className={classNames( 'tweet-length__text', exceededMax && 'red' )} > {charsLeft} </span> )} </div> )} {!isInputEmpty && <hr className="divider" />} <button type="submit" className="submit-btn" disabled={isInputEmpty} > {submitText} </button> </div> </div> </div> </Form> </Container> ) }

Create the Left Section

Left side section
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

The left section shows the different navigation links, the "Tweet" button, and the user icon at the bottom.

Create a new file called src/components/LeftSide.js. Add the following imports:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import classNames from 'classnames' import { useEffect, useState } from 'react' import { useStreamContext } from 'react-activity-feed' import { Link, useLocation } from 'react-router-dom' import styled from 'styled-components' import LoadingIndicator from './LoadingIndicator' import Bell from './Icons/Bell' import Group from './Icons/Group' import Home from './Icons/Home' import Hashtag from './Icons/Hashtag' import Mail from './Icons/Mail' import Bookmark from './Icons/Bookmark' import User from './Icons/User' import More from './Icons/More' import Twitter from './Icons/Twitter'

useStreamContext is an exported custom hook from the react-activity-feed library, which exposes context data from the StreamApp component you added in App.js. From this hook, you can get the logged-in user details.

You will use the useLocation hook to get information about the URL path, which can be useful for getting the active link.

Next, the styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
const Container = styled.div` display: flex; flex-direction: column; padding: 0 30px; height: 100%; .header { padding: 15px; } .buttons { margin-top: 5px; max-width: 200px; a, button { display: block; margin-bottom: 12px; color: white; padding: 10px 15px; display: flex; align-items: center; border-radius: 30px; font-size: 18px; padding-right: 25px; text-decoration: none; --icon-size: 25px; .btn--icon { margin-right: 15px; height: var(--icon-size); width: var(--icon-size); position: relative; .notifications-count { position: absolute; font-size: 11px; /* min-width: 14px; */ background-color: var(--theme-color); top: -5px; padding: 1px 5px; border-radius: 10px; left: 0; right: 0; margin: 0 auto; width: max-content; } } &.active { font-weight: bold; img { --size: 27px; } } &:hover { background-color: #333; } &.btn--home { position: relative; &.new-tweets::after { content: ''; position: absolute; width: 5px; height: 5px; left: 35px; top: 7px; border-radius: 50%; background-color: var(--theme-color); } } &.btn--more { svg { border: 1px solid #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; } } } } .tweet-btn { background-color: var(--theme-color); margin-top: 10px; border-radius: 30px; color: white; text-align: center; padding: 15px 0; font-size: 16px; } .profile-section { margin-top: auto; margin-bottom: 20px; padding: 10px; display: flex; text-align: left; align-items: center; justify-content: space-between; border-radius: 30px; &:hover { background-color: #333; } .details { display: flex; align-items: center; &__img { margin-right: 10px; width: 40px; border-radius: 50%; height: 40px; overflow: hidden; img { width: 100%; height: 100%; } } &__text { span { display: block; } &__name { color: white; font-size: 16px; font-weight: bold; } &__id { font-size: 14px; margin-top: 2px; color: #aaa; } } } } `

Add the following to the LeftSide.js file:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
export default function LeftSide({ onClickTweet }) { const location = useLocation() const { userData } = useStreamContext() const [newNotifications, setNewNotifications] = useState(0) if (!userData) return ( <Container> <LoadingIndicator /> </Container> ) const menus = [ { id: 'home', label: 'Home', Icon: Home, link: '/home', }, { id: 'explore', label: 'Explore', Icon: Hashtag, }, { id: 'communities', label: 'Communities', Icon: Group, }, { id: 'notifications', label: 'Notifications', Icon: Bell, link: '/notifications', value: newNotifications, }, { id: 'messages', label: 'Messages', Icon: Mail, }, { id: 'bookmarks', label: 'Bookmarks', Icon: Bookmark, }, { id: 'profile', label: 'Profile', Icon: User, link:

The component receives an onClickTweet method prop which is called when the "Tweet" button is clicked.

First, you get the user object from useStreamContext. Also, you keep track of the notifications state.

Note:Part 3 of this series focuses on implementing notifications.

You also show the LoadingIndicator component if the userData object is undefined.

And you have the menu list. Now, for the UI:

js
1
2
3
4
5
6
7
8
9
10
11
12
function App({ onClickTweet }) { // ... return ( <Container> <Link to="/" className="header"> <Twitter color="white" size={25} /> </Link> <div className="buttons"> {menus.map((m) => { const isActiveLink = location.pathname ===

For the link, you determine if it is active if the id of the item in the menu is the same as the pathname of the URL. For the profile, you check if the pathname of the URL is the same as the id of the logged-in user.

With the left side done, you can proceed to the right side of the layout. The right side has a "Follow" button, so first, create a shareable follow button component.

Create the Right Section

Right side section

The right section shows the search input, the "Trends for you" block, and the "Who to follow" block.

Create a new file src/components/RightSide.js. Add the following imports:

js
1
2
3
4
5
6
7
8
9
10
import classNames from 'classnames' import { useState } from 'react' import { useStreamContext } from 'react-activity-feed' import { Link } from 'react-router-dom' import styled from 'styled-components' import users from '../users' import FollowBtn from './FollowBtn' import More from './Icons/More' import Search from './Icons/Search'

Next, you have the trends demo data:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const trends = [ { title: 'iPhone 12', tweetsCount: '11.6k', category: 'Technology', }, { title: 'LinkedIn', tweetsCount: '51.1K', category: 'Business & finance', }, { title: 'John Cena', tweetsCount: '1,200', category: 'Sports', }, { title: '#Microsoft', tweetsCount: '3,022', category: 'Business & finance', }, { title: '#DataSciencve', tweetsCount: '18.6k', category: 'Technology', }, ]

Now for the component:

js
1
2
3
4
5
6
7
8
9
10
export default function RightSide() { const [searchText, setSearchText] = useState('') const { client } = useStreamContext() const whoToFollow = users.filter((u) => { // filter out currently logged in user return u.id !== client.userId }) }

You keep track of the searchText state and also have the whoToFollow array, which is the users array with the currently logged-in user filtered out.

For the UI, paste the following:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
export default function RightSide() { // ... return ( <Container> <div className="search-container"> <form className="search-form"> <div className="search-icon"> <Search color="rgba(85,85,85,1)" /> </div> <input onChange={(e) => setSearchText(e.target.value)} value={searchText} /> <button className={classNames(!Boolean(searchText) && 'hide', 'submit-btn')} type="button" onClick={() => setSearchText('')} > X </button> </form> </div> <div className="trends"> <h2>Trends for you</h2> <div className="trends-list"> {trends.map((trend, i) => { return ( <div className="trend" key={trend.title + '-' + i}> <div className="trend__details"> <div className="trend__details__category"> {trend.category} <span className="trend__details__category--label"> Trending </span> </div> <span className="trend__details__title">{trend.title}</span> <span className="trend__details__tweets-count"> {trend.tweetsCount} Tweets </span> </div> <button className="more-btn"> <More color="white" /> </button> </div> ) })} </div> </div> <div className="follows"> <h2>Who to follow</h2> <div className="follows-list"> {whoToFollow.map((user) => { return ( <div className="user" key={user.id}> <Link to={

The UI shows the search input and loops through the trends and whoToFollow array and displays them on the UI.

Create the Layout Component

The Layout component shows the create tweet modal, so before the layout, create this component.

Create a CreateTweetDialog Component

Create a new file src/components/Tweet/CreateTweetDialog.js. Start with the import and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import styled from 'styled-components' import Modal from '../Modal' import TweetForm from './TweetForm' const Container = styled.div` .modal-block { margin-top: 20px; padding: 15px; width: 600px; height: max-content; z-index: 10; } .tweet-form { margin-top: 20px; } `

The shareable TweetForm component will be used in this component. Next, the UI:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default function CreateTweetDialog({ onClickOutside }) { const onSubmit = async (text) => { // create tweet onClickOutside() } return ( <Container> <Modal onClickOutside={onClickOutside} className="modal-block"> <TweetForm onSubmit={onSubmit} shouldFocus={true} minHeight={240} className="tweet-form" placeholder="What's happening" /> </Modal> </Container> ) }

The create tweet function itself will be created in a few sections below; this is just the layout.

Compose With the Layout Component

With the LeftSide, RightSide, and tweet modal components ready, you can create the Layout component.

Create a new file src/components/Layout.js. Add the imports:

js
1
2
3
4
5
6
7
8
import { useState } from 'react' import { useStreamContext } from 'react-activity-feed' import styled from 'styled-components' import LeftSide from './LeftSide' import CreateTweetDialog from './Tweet/CreateTweetDialog' import RightSide from './RightSide' import LoadingIndicator from './LoadingIndicator'

The styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const Container = styled.div` min-height: 100vh; background: black; --left: 300px; --right: 400px; --middle: calc(100% - var(--left) - var(--right)); .content { max-width: 1300px; margin: 0 auto; width: 100%; display: flex; } .left-side-bar { height: 100vh; width: var(--left); position: sticky; top: 0; } .main-content { position: relative; width: var(--middle); border-left: 1px solid #333; border-right: 1px solid #333; min-height: 100vh; } .right-side-bar { width: var(--right); } `

The Container styled component has three style variables: --left of 300px, --right of 400px, and --middle, which is calculated by subtracting the left and right from 100%. The left section uses the left variable, and so for the right and the middle content.

For the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export default function Layout({ children }) { const { user } = useStreamContext() const [createDialogOpened, setCreateDialogOpened] = useState(false) if (!user) return <LoadingIndicator /> return ( <> {createDialogOpened && ( <CreateTweetDialog onClickOutside={() => setCreateDialogOpened(false)} /> )} <Container> <div className="content"> <div className="left-side-bar"> <LeftSide onClickTweet={() => setCreateDialogOpened(true)} /> </div> <main className="main-content"> {!user ? <LoadingIndicator /> : children} </main> <div className="right-side-bar"> <RightSide /> </div> <div /> </div> </Container> </> ) }

The Layout component manages a createDialogOpened state which is updated to true when the "Tweet" button in the LeftSide component is clicked.

Create the HomeContent Component

Home content component

This component will show the logged-in user's timeline. Their timeline shows the tweets of people they follow.

The HomeContent component houses the top header, the tweet form below the header, and the timeline feed. Let us start from the header.

Create the Home Top Header component

Create a new file src/components/Home/MainHeader.js with the following code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import styled from 'styled-components' import Star from '../Icons/Star' const Header = styled.header` display: flex; align-items: center; padding: 15px; color: white; width: 100%; font-weight: bold; justify-content: space-between; backdrop-filter: blur(2px); background-color: rgba(0, 0, 0, 0.5); h1 { font-size: 20px; } ` export default function MainHeader() { return ( <Header> <h1>Home</h1> <Star color="white" /> </Header> ) }

Create the CreateTweetTop component

The CreateTweetTop component shows the tweet form below the header. This component will also use the shareable TweetForm component.

Create a new file, src/components/Home/CreateTweetTop.js with the following code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import styled from 'styled-components' import TweetForm from '../Tweet/TweetForm' const Container = styled.div` padding: 15px; ` export default function CreateTweetTop() { const onSubmit = async (text) => { // create tweet here } return ( <Container> <TweetForm placeholder="What's happening?" onSubmit={onSubmit} /> </Container> ) }

The onSubmit method does nothing for you. Later on in this article, you will add the functionality to create a tweet. For now, let us focus on the layout.

For the remaining part of the HomeContent component, you also need a tweet block that shows a tweet's information, actor details, and reactions.

Create the TweetBlock component

The TweetBlock component is broken down into three elements: TweetActorName, the tweet's content, and a CommentDialog modal component.

Tweet block with actor name, content and actions
Create the TweetActorName component

The TweetActorName is a shared component that shows the name and id of an actor. It also shows the time (hours difference or date) that the tweet was made. Create a new file called src/components/Tweet/TweetActorName.js.

Add the imports and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { format } from 'date-fns' import { Link } from 'react-router-dom' import styled from 'styled-components' const TextBlock = styled(Link)` display: flex; &:hover .user--name { text-decoration: underline; } .user { &--name { color: white; font-weight: bold; } &--id { margin-left: 5px; color: #777; } } .tweet-date { margin-left: 15px; color: #777; position: relative; &::after { content: ''; width: 2px; height: 2px; background-color: #777; position: absolute; left: -8px; top: 0; bottom: 0; margin: auto 0; } } `

And the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default function TweetActorName({ time, name, id }) { const timeDiff = Date.now() - new Date(time).getTime() // convert ms to hours const hoursBetweenDates = timeDiff / (60 * 60 * 1000) const lessThan24hrs = hoursBetweenDates < 24 const lessThan1hr = hoursBetweenDates < 1 const timeText = lessThan1hr ? format(timeDiff, 'm') + 'm' : lessThan24hrs ? format(timeDiff, 'H') + 'h' : format(new Date(time), 'MMM d') return ( <TextBlock to={

The time is interpreted in three ways. If it is less than one hour, it shows as "[X]m". If it is less than twenty-four hours, it shows as "[X]h". And if it is none of these conditions, it is displayed as "Month Date".

Create a Tweet Link Generator Utility

Tweet links usually exist in this format: /{username}/status/{tweet-id}/. You will create a reusable function that generates a link like this.

Create a new file src/utils/links.js with the following code:

js
1
2
export function generateTweetLink(actorId, tweetActivityId) { return
Create a Text Formatter Utility for Links

Because texts can contain links, hashtags, and mentions, you will create a utility for formatting such texts and replacing some of the texts with anchor tags.

Create a new file src/utils/string.js. And add the following function:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function formatStringWithLink(text, linkClass, noLink = false) { // regex to match links, hashtags and mentions const regex = /((https?:\/\/\S*)|(#\S*))|(@\S*)/gi const modifiedText = text.replace(regex, (match) => { let url, label if (match.startsWith('#')) { // it is a hashtag url = match label = match } else if (match.startsWith('@')) { // it is a mention url =

This utility returns an HTML string that can be embedded into an element.

Create the CommentDialog Component

The CommentDialog modal popups up when the comment icon is clicked on a tweet block:

This dialog will be used to add a comment to a tweet. Create a new file src/components/Tweet/CommentDialog. Let us start with the imports and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import styled from 'styled-components' import { formatStringWithLink } from '../../utils/string' import Modal from '../Modal' import TweetActorName from './TweetActorName' import TweetForm from './TweetForm' const Container = styled.div` .modal-block { padding: 15px; width: 600px; height: max-content; } ` const BlockContent = styled.div` .tweet { margin-top: 30px; display: flex; position: relative; &::after { content: ''; background-color: #444; width: 2px; height: calc(100% - 35px); position: absolute; left: 20px; z-index: 0; top: 45px; } .img { width: 40px; height: 40px; border-radius: 50%; margin-right: 15px; border-radius: 50%; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } } .details { .actor-name { font-size: 15px; &--name { color: white; font-weight: bold; } &--id { color: #888; } } .tweet-text { color: white; margin-top: 3px; font-size: 14px; } .replying-info { color: #555; display: flex; margin-top: 20px; font-size: 14px; &--actor { margin-left: 5px; color: var(--theme-color); } } } } .comment { display: flex; margin-top: 20px; .img { width: 35px; height: 35px; margin-left: 3px; border-radius: 50%; margin-right: 15px; border-radius: 50%; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } } .comment-form { flex: 1; height: 120px; } } `

This component uses the shareable TweetForm and TweetActorName components.

Next, the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default function CommentDialog({ activity, onPostComment, onClickOutside, }) { const { object: { data: tweet }, } = activity const tweetActor = activity.actor const onSubmit = async (text) => { await onPostComment(text) onClickOutside() } }

This component receives three props:

  • activity: The active activity that the comment should be added to
  • onPostComment: A function called with the text argument when the submit button from the TweetForm component is called
  • onClickOutside: A function called when the backdrop of the modal is called

Now, for the UI:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
export default function CommentDialog( { // ... } ) { // ... return ( <Container> <Modal onClickOutside={onClickOutside} className="modal-block"> <BlockContent> <div className="tweet"> <div className="img"> <img src={tweetActor.data.image} alt="" /> </div> <div className="details"> <TweetActorName time={activity.time} name={tweetActor.data.name} id={tweetActor.data.id} /> <p className="tweet-text" dangerouslySetInnerHTML={{ __html: formatStringWithLink( tweet.text, 'tweet__text--link', true ).replace(/\n/g, '<br/>'), }} /> <div className="replying-info"> Replying to{' '} <span className="replying-info--actor">@{tweetActor.id}</span> </div> </div> </div> <div className="comment"> <TweetForm className="comment-form" submitText="Reply" placeholder="Tweet your reply" onSubmit={onSubmit} shouldFocus={true} /> </div> </BlockContent> </Modal> </Container> ) }
Composing the TweetBlock Component

With the required components created, you can now compose this component.

Create a new file, src/components/Tweet/TweetBlock.js. Start with the imports:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import classNames from 'classnames' import { useState } from 'react' import { useStreamContext } from 'react-activity-feed' import { useNavigate } from 'react-router-dom' import styled from 'styled-components' import { formatStringWithLink } from '../../utils/string' import CommentDialog from './CommentDialog' import Comment from '../Icons/Comment' import Heart from '../Icons/Heart' import Retweet from '../Icons/Retweet' import Upload from '../Icons/Upload' import More from '../Icons/More' import TweetActorName from './TweetActorName' import { generateTweetLink } from '../../utils/links'

Next, paste the styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
const Block = styled.div` display: flex; border-bottom: 1px solid #333; padding: 15px; .user-image { width: 40px; height: 40px; border-radius: 50%; overflow: hidden; margin-right: 10px; img { width: 100%; height: 100%; object-fit: cover; } } .tweet { flex: 1; .link { display: block; padding-bottom: 5px; text-decoration: none; width: 100%; } &__text { color: white; font-size: 15px; line-height: 20px; margin-top: 3px; width: 100%; &--link { color: var(--theme-color); text-decoration: none; } } &__actions { display: flex; justify-content: space-between; margin-top: 5px; button { display: flex; align-items: center; } &__value { margin-left: 10px; color: #666; &.colored { color: var(--theme-color); } } } &__image { margin-top: 20px; border-radius: 20px; border: 1px solid #333; overflow: hidden; width: calc(100% + 20px); width: 100%; height: 100%; object-fit: cover; object-position: center; } } .more { width: 40px; height: 40px; display: flex; } `

Then the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
export default function TweetBlock({ activity }) { const { user } = useStreamContext() const navigate = useNavigate() const [commentDialogOpened, setCommentDialogOpened] = useState(false) const actor = activity.actor let hasLikedTweet = false const tweet = activity.object.data // check if current logged in user has liked tweet if (activity?.own_reactions?.like) { const myReaction = activity.own_reactions.like.find( (l) => l.user.id === user.id ) hasLikedTweet = Boolean(myReaction) } const onToggleLike = () => { // toggle like reaction } const actions = [ { id: 'comment', Icon: Comment, alt: 'Comment', value: activity?.reaction_counts?.comment || 0, onClick: () => setCommentDialogOpened(true), }, { id: 'retweet', Icon: Retweet, alt: 'Retweet', value: 0, }, { id: 'heart', Icon: Heart, alt: 'Heart', value: activity?.reaction_counts?.like || 0, onClick: onToggleLike }, { id: 'upload', Icon: Upload, alt: 'Upload', }, ] const tweetLink = activity.id ? generateTweetLink(actor.id, activity.id) : '#' const onPostComment = async (text) => { // create comment } }

The hasLikedTweet variable is a boolean that indicates if the currently logged-in user has liked the current tweet. To find this information, you check the like object of the own_reactions object of the activity. The like object holds an array of objects which contains information about users that have added a like reaction to an activity.

The onToggleLike and onPostComment functions do nothing just yet. Part 3 covers adding reactions.

Next for this component is the UI:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
export default function TweetBlock({ activity }) { //... return ( <> <Block> <div className="user-image"> <img src={actor.data.image} alt="" /> </div> <div className="tweet"> <button onClick={() => navigate(tweetLink)} className="link"> <TweetActorName name={actor.data.name} id={actor.id} time={activity.time} /> <div className="tweet__details"> <p className="tweet__text" dangerouslySetInnerHTML={{ __html: formatStringWithLink( tweet.text, 'tweet__text--link' ).replace(/\n/g, '<br/>'), }} /> </div> </button> <div className="tweet__actions"> {actions.map((action) => { return ( <button onClick={(e) => { e.stopPropagation() action.onClick?.() }} key={action.id} type="button" > <action.Icon color={ action.id === 'heart' && hasLikedTweet ? 'var(--theme-color)' : '#777' } size={17} fill={action.id === 'heart' && hasLikedTweet && true} /> <span className={classNames('tweet__actions__value', { colored: action.id === 'heart' && hasLikedTweet, })} > {action.value} </span> </button> ) })} </div> </div> <button className="more"> <More color="#777" size={20} /> </button> </Block> {activity.id && commentDialogOpened && ( <CommentDialog onPostComment={onPostComment} shouldOpen={commentDialogOpened} onClickOutside={() => setCommentDialogOpened(false)} activity={activity} /> )} </> ) }

This UI shows the tweet block with the action buttons (comment, like) and the comment dialog when it is active. On submitting the tweet form in the comment dialog, nothing happens for now. You will add this functionality in Part 3.

Creating the Timeline Component

The Timeline component shows the tweets made by the users the currently logged-in user follows:

Since we haven't added the follow feature yet, you will create this component to show the tweets made by the currently logged-in user.

Create a new file src/components/Home/Timeline.js with the following code:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
import { FlatFeed, useStreamContext } from 'react-activity-feed' import TweetBlock from '../Tweet/TweetBlock' export default function Timeline() { const { user } = useStreamContext() return ( <div> <FlatFeed Activity={TweetBlock} userId={user.id} feedGroup="user" /> </div> ) }

The FlatFeed component allows you to pass a custom Activity component using the Activity prop.

Also, in the FlatFeed component, you can use the "timeline" or "user" feedGroup. The "timeline" shows a feed of activities made by the users a user follows. While the "user", similar to a user's profile page, shows a feed of activities made by a particular user (the logged-in user in our case). For now, we will leave this as "user". You will change this to "timeline" when you add the follow feature.

Composing the HomeContent Component

You can now compose the HomeContent component with the dialog, utilities, timeline, and other components created.

Create a new file src/components/Home/HomeContent.js. Add the import and styles:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import styled from 'styled-components' import { Feed, useStreamContext } from 'react-activity-feed' import CreateTweetTop from './CreateTweetTop' import MainHeader from './MainHeader' import Timeline from '../Home/Timeline' import LoadingIndicator from '../LoadingIndicator' const Container = styled.div` .header { position: sticky; top: 0; z-index: 1; } .create-tweet-top { border-bottom: 1px solid #333; } .new-tweets-info { border-bottom: 1px solid #333; padding: 20px; text-align: center; color: var(--theme-color); display: block; width: 100%; font-size: 16px; &:hover { background: #111; } } `

The Feed component does not add anything to the UI. It provides feed data and methods such that the children of these components can create tweets in the user's feed.

Next, the component:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export default function HomeContent() { const { client } = useStreamContext() const user = client.currentUser.data if (!user) return ( <Container> <LoadingIndicator /> </Container> ) return ( <Container> <div className="header"> <MainHeader /> </div> <Feed feedGroup="user"> <div className="create-tweet-top"> <CreateTweetTop /> </div> <Timeline /> </Feed> </Container> ) }

Create a Page Component for the Homepage

With the layout and home content components ready, you can now create a page for the home content.

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

js
1
2
3
4
5
6
7
8
9
10
import Layout from '../components/Layout' import HomeContent from '../components/Home/HomeContent' export default function Home() { return ( <Layout> <HomeContent /> </Layout> ) }

In App.js, add a route for the home page like this:

js
1
2
// other imports import HomePage from './pages/HomePage'
js
1
2
// other routes <Route element={<HomePage />} path="/home" />

With your development server on, when you go to localhost:3000/home, you will see the Homepage result.

When you click the "Tweet" button on the left section, you can also see the create tweet modal.

For now, you cannot see the comment dialog as the tweet block is not in use. Next, I will walk you through adding the create tweet feature so you can see the other components at work.

Add a Create Tweet Feature

In this section, you add the create tweet feature that allows users to create tweets. After adding this feature and using it, you can see the TweetBlock components in the Timeline component.

Create a Custom useTweet Hook

The tweet feature can be triggered from the CreateTweetDialog and the CreateTweetTop components. Creating a custom hook for this feature makes things manageable.

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

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { nanoid } from 'nanoid' import { useStreamContext } from 'react-activity-feed' export default function useTweet() { const { client } = useStreamContext() const user = client.feed('user', client.userId) const createTweet = async (text) => { const collection = await client.collections.add('tweet', nanoid(), { text }) await user.addActivity({ verb: 'tweet', object:

In this hook, you retrieve the client object from useStreamContext. With the client object, you can instantiate the user's feed.

The createTweet function receives a text argument, and in that function, you create a tweet collection with the text data in an object. Then, you create an activity on the user's feed, with the collection id passed to the object property. This property receives a reference to a collection, which you have specified as a tweet reference, and the collection's id.

Now you can use the createTweet function in other components.

Add the useTweet Hook to the CreateTweetDialog Component

In the src/components/Tweet/CreateTweetDialog.js component file, import the hook:

js
1
2
// other imports import useTweet from '../../hooks/useTweet'

Then, use the hook in the component:

js
1
2
3
4
5
6
7
8
9
10
11
export default function CreateTweetDialog({ onClickOutside }) { const { createTweet } = useTweet() const onSubmit = async (text) => { createTweet(text) onClickOutside() } return // the UI }

Add the useTweet Hook to the CreateTweetTop Component

In the src/components/Home/CreateTweetTop.js component file, import the hook:

js
1
2
// other imports import useTweet from '../../hooks/useTweet'

Then, use the hook in the component:

js
1
2
3
4
5
6
7
8
9
export default function CreateTweetTop() { const { createTweet } = useTweet() const onSubmit = async (text) => { createTweet(text) } return // the UI }

And now, you can create tweets. Click on "Tweet" in the left section of the screen, and create your first tweet in the modal.

Create tweet modal

On submitting and refreshing, you will see the homepage showing the new tweet.

Conclusion

In this tutorial, you have successfully created the Twitter layout using the React Activity Feed SDK. This project currently allows a user to select a profile and authenticate them with the feeds application in your Stream dashboard. It also includes reusable components, and the create tweet feature.

Next on this series, check out part two where you learn how to add a profile page for users and integrate the follow-user functionality.

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->