Deploy a React Chat App to Heroku

Richard U.
Richard U.
Published March 20, 2020 Updated August 28, 2020

In this article, we will be creating a chat application using React and Stream Chat. The app will feature an authorization page for login/signup, followed by a chat view that allows for communication between several authorized users. After creating the chat application, we will deploy it to Heroku, to take it public.

The final application we build will look like this:

Stream Chat - Example

Prerequisites

To prepare yourself to follow along with this tutorial, you'll need the following:

Stream Setup

We’ll be making use of Stream Chat components; to start using Stream, we have to create an account and an application. Creating an app will provide the API-key and token needed to initialize the chat components.

Visit the signup page to create a Stream account, if you don’t have one already; if you do have an account, you can log in here. Once you’re logged in, you can use the KEY and SECRET from app that's automatically created for you. Or, if you prefer, you can go to the dashboard, where you can access your apps and private keys. Copy the KEY and SECRET of your application; we’ll be making use of these in the following sections.

Stream Dashboard

It is essential to keep your keys private; we'll look at where you can securely keep them in the next section.

Setting Up the Server

Our application will have a view for authenticating users. To facilitate this, we’ll need a server to handle the authentication and authorize the user on Stream. We’ll be making use of an open-source solution that has a server running on Express and MongoDB, using the Stream API for token generation for new and existing users on your Stream Chat instance. You can find the project repository here; clone the repo into your local machine using the following command:

sh
1
$ git clone https://github.com/astrotars/stream-chat-api

After cloning the repository, cd into the new folder created and install the project’s dependencies using the following command:

sh
1
$ npm install

OR

sh
1
$ yarn

After installing the dependencies, locate the env.example file, copy its contents and create a new .env file and paste the copied contents inside it. The contents of the .env.example file should look like the snippet below:

NODE_ENV=development
PORT=8080

# Replace the placeholder values with the real values
STREAM_API_KEY=YOUR_STREAM_API_KEY
STREAM_API_SECRET=YOUR_STREAM_API_SECRET
MONGODB_URI=YOUR_MONGODB_URI

Replace the placeholder values with the keys you copied from the Stream dashboard. For the MONGODB_URI, you can use your local database URI or the connection URI provided after creating a cluster on MongoDB Atlas.

To start the server in development mode, run the following command:

sh
1
$ npm run dev

Setting Up the Application

The frontend application will be built using React, so we can bootstrap it using the create-react-app CLI. Run the following command to create a bootstrapped React application.

For NPX:

sh
1
$ npx create-react-app stream-chat-app

For Yarn:

sh
1
$ yarn create react-app stream-chat-app

Once the command has run to completion, open the newly created folder, and install the packages using the command below:

sh
1
$ npm install stream-chat-react stream-chat react-router-dom

which installs:

Styles

For styling, we will make use of TailwindCSS. Tailwind is a utility-first CSS framework for creating custom designs. It provides utility classes for use within applications, and it is framework agnostic.

Install TailwindCSS using the following command:

sh
1
npm install tailwindcss --save-dev

After installing the package, create a file called tailwind.css in the root folder of your stream-chat-app directory. Next, Open the tailwind.css file and include the following build directives for Tailwind:

@tailwind base;
@tailwind components;
@tailwind utilities;

The directives above will inject Tailwind's base, components, and utilities styles into your CSS. They will be swapped at build time with all of Tailwind's pre-generated CSS.

Open the package.json file and update the scripts section to look like the snippet below:

"scripts": {
  "start": "npm run run:css && react-scripts start",
  "run:css": "npx tailwind build tailwind.css -o vendor.css"
},
...

The start command runs the run:css command to build the stylesheet before it starts the development server.

The run:css command generates and outputs Tailwind’s CSS code to a vendor.css file using the directives we provided in the tailwind.css file.

Before starting the application, we’ll import the generated stylesheet into the App.css file and add an external font in the index.html file. Open the App.css file and update it to include the following:

@import '~stream-chat-react/dist/css/index.css';
@import './vendor.css';
* {
  font-family: 'Raleway', sans-serif;
}

Above, we import the style sheet for the Stream components and the vendor.css file. Next, open the index.html file and add the external font we’ll be making use of:

...
<link
  href="https://fonts.googleapis.com/css?family=Raleway:300,400,600,700,900&display=swap"
  rel="stylesheet"
/>
...

After completing the setup, run npm start to start the development server. Running this command should point your browser to http://localhost:3000. In the next section, we will begin setting up the authentication view!

Setting Up the Authentication View

The authentication page will feature a simple login/signup form; when the user completes the form, they get redirected to the home page, where the chat view resides. Within the ./src directory, create a directory named "pages". This directory will hold different aspects of our application.

In the pages directory, create an auth directory to hold the authentication page component. Create a file named index.js in the ./src/pages/auth directory and add the following content to it:

import React, { useState, useContext } from 'react';
import AuthImg from './undraw_authentication_fsn5.svg';
import { AppContext } from '../../contexts';
import { Redirect } from 'react-router-dom';

const useInput = (props, labelText) => {
  const [value, setValue] = useState('');
  const input = (
    <>
      <label className='text-xs text-gray-700 font-bold'>{labelText}</label>
      <input
        {...props}
        value={value}
        onChange={e => setValue(e.target.value)}
        className='bg-gray-200 appearance-none border-2 border-gray-200 rounded w-full py-2 px-4 text-gray-700 leading-tight focus:outline-none focus:bg-white focus:border-purple-500'
      />
    </>
  );
  return [value, input];
};

const Auth = () => {
  const { setUserData } = useContext(AppContext);
  const [authComplete, setAuthComplete] = useState(false);
  const [authState, setAuthState] = useState('login');
  const [firstName, firstNameInput] = useInput(
    { type: 'text', placeholder: 'First Name' },
    'First Name'
  );
  const [lastName, lastNameInput] = useInput(
    { type: 'text', placeholder: 'Last Name' },
    'Last Name'
  );
  const [email, emailInput] = useInput(
    { type: 'email', placeholder: 'Email' },
    'Email'
  );
  const [password, passwordInput] = useInput(
    { type: 'password', placeholder: 'Password' },
    'Password'
  );
  const handleSubmit = async e => {
    ... // submit handler for the form
  };

  return (
    ... // return value
  );
};
export default Auth;

This is the skeleton of the component. We still have to fill in the return value of the component and the event handler for the submit event of the form. The useInput prop aims to reduce repetition when creating form input elements.

We also made use of a context value "setUserData"; we will go into details about setting up the context and the provider in the next section, but first, let’s add the return value for the Auth component. Use the snippet below as the return value for the component:

...
return authComplete ? (
    <Redirect to='/' />
  ) : (
    <div className='w-11/12 sm:w-4/5 md:w-1/3 mx-auto'>
      <div className='mt-20 flex flex-col items-center justify-center'>
        <img src={AuthImg} alt='Login first' className='h-32 max-w-full' />
        <h2 className='text-xl mt-4 font-semibold text-gray-700 capitalize'>
          {authState}
        </h2>
      </div>
      <form className='px-5 py-6 w-4/5 mx-auto' onSubmit={handleSubmit}>
        {authState === 'signup' && (
          <>
            <div className='flex'>
              <div className=''>{firstNameInput}</div>
              <div className='ml-4'>{lastNameInput}</div>
            </div>
          </>
        )}
        <div>{emailInput}</div>
        <div className='mt-3'>{passwordInput}</div>
        <div className='mt-4 flex justify-center'>
          <button className='w-1/2 py-2 text-base text-white bg-purple-700 shadow-lg font-bold hover:bg-purple-600'>
            Submit
          </button>
        </div>
        <div className='mt-2'>
          {authState === 'signup' ? (
            <p className='text-sm text-gray-600 font-semibold'>
              Already have an account?{' '}
              <button
                className='ml-1 text-purple-600 text-base text-bold'
                onClick={e => setAuthState('login')}
                type='button'
              >
                Login
              </button>
            </p>
          ) : (
            <p className='text-sm text-gray-600 font-semibold'>
              Don't have an account?{' '}
              <button
                className='ml-1 text-purple-600 text-base text-bold'
                onClick={e => setAuthState('signup')}
                type='button'
              >
                Register
              </button>
            </p>
          )}
        </div>
      </form>
    </div>
  );
...

In the return value, we add a check (using the authComplete value) for when the authentication is complete, and then we redirect to the / route, which renders the chat view.

When the user fills the form and submits it, we send the information to the server we set up in the previous step. The server supports user authentication, so when we send the user information there, it is stored and exchanged for a token, which we’ll use for connecting to the Stream client.

Update the body of the handleSubmit function with the following:

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
...
const handleSubmit = async e => {
  e.preventDefault();
  const payload = {
    name: {
      first: firstName,
      last: lastName
    },
    email: email,
    password: password
  };
  try {
    const response = await (
      await fetch('http://localhost:8080/v1/auth/init', {
        method: 'POST',
        headers: {
          'content-type': 'application/json'
        },
        body: JSON.stringify(payload)
      })
    ).json();
    setUserData(response);
    setAuthComplete(true);
  } catch (err) {
    console.log(err);
  }
};
...

In the submit handler, we create the payload that consists of the name, email and password. We send a POST request to the server with the payload as the request body. After the request is complete, we pass the response from the request as an argument to the setUserData function.

The setUserData function takes an object representing the details of the current user and stores it in the context, making it available for use throughout the application.

In the next steps, we’ll set up the context, the provider, and methods for updating the state of the application.

Creating the Context and Provider

Context is used to share data considered to be “global” for several components within an application. To create a context object, we make use of the createContext method. Create a new directory within the src directory named contexts and, within the src/contexts folder, create an index.js file. Open the file using any editor and copy the following code into it:

import { createContext } from 'react';
export const AppContext = createContext({
  userData: null,
  setUserData: user => {}
});

In the snippet above, we create the context object using createContext by passing in an object as the initial state. The initial state consists of the userData and a method for setting the userData value (setUserData).

Next, we’ll create a provider to make the values of this context object available application-wide. Start by creating a providers directory in the src directory; within the providers folder, create an index.js file. In the file, we’ll create a provider component and create some state values within the component using hooks. Open the index.js file with your editor and copy the following code into the file:

import React, { useState } from 'react';
import { AppContext } from '../contexts';

const AppProvider = ({ children }) => {
  const [userData, setUserData] = useState(null);
  const value = {
    userData,
    setUserData
  };

  return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
export default AppProvider;

The AppProvider component is a lightweight component with a single state value and a function for updating the state. The component returns the provider of the AppContext, and the userData state value and its accompanying update function (setUserData) are passed as values to the provider.

Let’s update the App.js file next, to render the AppProvider and the Auth route. We’ll make use of the React Router to create routes for our application. Open the App.js file and update it to look like the snippet below:

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';

import Auth from './pages/auth';
import AppProvider from './providers';
import './App.css';

const App = () => {
  return (
    <AppProvider>
      <Router>
        <Switch>
          <Route path='/auth' component={Auth} />
        </Switch>
      </Router>
    </AppProvider>
  );
};
export default App;

Now, we have the context object ready, the provider component giving the application access to the context values, and the auth route available; let’s head to the browser and see how the auth page looks!

Navigate to http://localhost:3000/auth in your browser; you should see a view similar to the screenshot below:

Login

We’re yet to create the home page with the chat view, so a successful signup/login will keep you on the same page. Let’s fix that!

In the next section, we will create the home page and make use of Stream’s components to create the chat interface.

Creating the Chat View

In this section, we will set up the home page, which houses the chat interface. The chat interface will be created using Stream’s components; the components offer features like:

  • Typing indicators
  • Rich media sharing
  • Message threads

To get started, create a directory called "home" within the ./src/pages directory, and then an index.js file in the ./src/pages/home folder. Open the new file and copy the snippet below into the file:

import React, { useEffect, useState, useContext } from 'react';
import { Redirect } from 'react-router-dom';
import {
  Chat,
  Channel,
  ChannelHeader,
  Thread,
  Window,
  MessageList,
  MessageInput
} from 'stream-chat-react';
import { StreamChat } from 'stream-chat';

import { AppContext } from '../../contexts';

let chatClient;

const Home = () => {
  const { userData: user } = useContext(AppContext);
  const [channel, setChannel] = useState(undefined);

  const setUser = () => {
   ... // set the user and channel herre
  };

  useEffect(() => {
    if (user) {
      setUser(user);
    }
  }, []);

  if (!user) {
    return <Redirect to='/auth' />;
  }

  return channel ? (
    <div className='w-1/3 mx-auto'>
      <Chat client={chatClient} theme={'messaging light'}>
        <Channel channel={channel}>
          <Window>
            <ChannelHeader />
            <MessageList />
            <MessageInput />
          </Window>
          <Thread />
        </Channel>
      </Chat>
    </div>
  ) : (
    <p>Loading...</p>
  );
};
export default Home;

In the component, we use the useContext hook to get the values from the AppContext. Calling the useContext hook with the AppContext as an argument returns the userData value. Since the Home route is protected, pending the availability of the user data, the component redirects to the /auth route if the userData isn’t available.

The component renders some Stream React components; let’s go through what each component does:

  • Chat- this component is the wrapper component for chat; it needs to wrap all the other chat components. The Chat component provides the ChatContext to all other components.
  • Channel - this component is the wrapper component for a channel. It needs to be placed inside the Chat component.
  • Window - is a UI component for displaying a chat thread or channel. It is useful for displaying a chat thread side by side with the main chat view.
  • ChannelHeader - displays some necessary information about the channel.
  • MessageList - renders a list of messages.
  • MessageInput - for typing new messages and for other actions like image uploads and emojis.

In the useEffect hook, we call the setUser function, passing the user object as an argument, if it is available. Let’s create the setUser function; replace the current setUser function with the complete version below:

const setUser = async ({ apiKey, user, token }) => {
  const { results: people } = await (
    await fetch('https://randomuser.me/api/?inc=picture')
  ).json();
  const [person] = people;
  const { picture } = person;

  chatClient = new StreamChat(apiKey);
  chatClient.setUser(
    {
      id: user._id,
      name: user.name.first,
      role: 'admin',
      image: picture.thumbnail
    },
    token
  );
  const channel = chatClient.channel('messaging', 'Chat');
  setChannel(channel);
};

In the setUser function, we fetch a profile picture to be used within the chat from the Random User API. After getting the photo, we initialize the Stream Chat client, using the apiKey contained in the user object, and assign it to the chatClient global variable.

After initializing the client, we call the setUser method on the client with the user fields as the first argument and the token as the second argument. Finally, we create a messaging channel and set it as a state value using the setChannel function.

The home component looks ready, so let’s create a route for it in the App.js file. Open the file with your editor and update it to look like the snippet below:

import React from 'react';
...
import Home from './pages/home'; // import the home component

const App = () => {
  return (
    <AppProvider>
      <Router>
        <Switch>
          <Route path='/auth' component={Auth} />
          <Route path='/' exact component={Home} /> // add the new home route
        </Switch>
      </Router>
    </AppProvider>
  );
};
export default App;

After this update, we can head back to the /auth route and start the authentication flow. Navigate to http://localhost:3000/auth to see our progress:

Side by Side

Deploying to Heroku

It is finally time to deploy our app to Heroku! If you don’t already have a Heroku account, you can create one here. After creating the account, you’ll need to install the Heroku CLI on your computer; select the download option compatible with your OS here.

After installing the CLI, you’ll have to log in; run the following command to log in using your browser:

sh
1
$ heroku login

After logging in successfully, you’ll have to create an application using the CLI; cd into the root folder of the application and run the following command:

sh
1
$ heroku create stream-chat-app

Next, we’ll set up a server running on Express to serve the application. Create a file called "server.js" in the root folder of the application, then open the file and update the file contents with the following:

const express = require('express');
const favicon = require('express-favicon');
const path = require('path');
const port = process.env.PORT || 8080;
const app = express();

app.use(favicon(__dirname + '/build/favicon.ico'));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, 'build')));

app.get('/*', function(req, res) {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

app.listen(port);

The code above creates a Node server that serves the build files. The /* route directs all requests to the index.html file.

Also, install the packages used in the server by running the following command:

sh
1
npm install express express-favicon

Next, we'll update our package.json scripts so that the start command will start the express server instead of the dev server. Update the scripts section of the package.json file to look like the snippet below:

...
"scripts": {
  "start": "node server",
  "start:dev": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},
...

Before we deploy the application, we’ll have to deploy the API that we're making our requests to. Luckily, the server has one-click deployment to Heroku available! Head over to the GitHub repository of the Stream Chat API and follow the deployment instructions to deploy the API.

After deploying the API, we'll need to grab the URL provided and update the application to make requests to the new endpoint. Head over to the ./src/pages/auth/index.js file and update the URL:

...

const handleSubmit = async e => {
  e.preventDefault();
  ...
  try {
    const response = await (
      await fetch('YOUR_NEW_API_ENDPOINT/v1/auth/init', {
        method: 'POST',
        headers: {
          'content-type': 'application/json'
        },
        body: JSON.stringify(payload)
      })
    ).json();
  ...
};

...

Replace YOUR_NEW_API_ENDPOINT with the endpoint you get after deploying the API. Preferably, it’ll be best to store this value as an environment variable (in your .env file).

Commit all your changes and run the command below to deploy the application:

sh
1
$ git push heroku master

When the command runs to completion, it means your application has been deployed successfully! Run heroku open to open the newly deployed application on your browser.

Wrapping Up

In this article, we looked at how we could create a messaging application using Stream and Stream React components. We also went through the authentication of users using the Stream Client and the API.

You can improve the application by persisting in the auth state, which means saving the information of the authenticated user locally using the local storage.

The source code of this application can be found on GitHub.

Happy coding!

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