Build a Customer Support Live Chat App With Laravel and React Hooks

8 min read
Eze S.
Eze S.
Published April 20, 2020 Updated October 23, 2020

Customer support live chat is an incredible tool for collecting valuable feedback from your customers and increasing your website engagement rate. With that said, it can take weeks or even months to build a functional, scalable, reliable real-time chat application. Luckily, Stream Chat makes it super easy to create such an app quickly; you can build a reliable live chat app in just hours with Stream's Chat API!

In this tutorial, we are going to build a customer support Live Chat app using Laravel, Stream Chat and ReactJS.

The full source code to this tutorial can be found on GitHub.

Prerequisites

To follow along with this tutorial, please ensure you have the following installed on your machine:

A basic understanding of Laravel, ReactJS, and JavaScript will also be helpful.

Setting Up Laravel and Stream

Run the command below to create a new Laravel application:

$ laravel new stream-laravel-react-livechat && cd stream-laravel-react-livechat

Setting Up Laravel Front-End Scaffolding for React

To begin setting up your front-end scaffolding, install a general laravel/ui package with Composer:

$ composer require laravel/ui:^2.0 --dev

To specify that you'll be using React to build your application, choose the React UI Package using the command below:

$ php artisan ui react

Once you are finished installing the required packages to make Laravel and React work together, you can proceed to set up your Stream Chat application with Laravel!

Setting Up Stream Chat with Laravel

First, you'll need to create an account on the Stream website. Head over to Stream to create a free account, by clicking on the SIGNUP button at the top right corner of the home page:

Stream Website

Once you create an account, you’ll be logged in and sent to your dashboard. In your dashboard, you'll find your API KEY, API SECRET and PRODUCTION APP ID, as shown below:

Stream Dashboard

Add these credentials from your Stream dashboard to your environment variables file (.env), like this:

MIX_STREAM_API_KEY=YOUR_STREAM_KEY
MIX_STREAM_API_SECRET=YOUR_STREAM_SECRET

The prefix MIX_ enables the React front end to access the environment variables.

Now that we've connected in our keys, let’s install the Stream Chat PHP client so that we can interact with the Stream API from Laravel. Run the command below to install the Stream client with composer:

$ composer require get-stream/stream-chat

Then, run yarn in your terminal to install node dependencies bootstrapped by Laravel.

We are now finished setting up Stream Chat with Laravel! Let’s move on to install Stream Chat on the front end, and to install React Router, as we’ll need it to navigate to different pages of the app.

Run the command below to install both of these packages:

$ yarn add stream-chat react-router-dom

We are now finished with the app set up! Let’s get into some coding!

To prepare ourselves to start coding, let’s compile our static files, start our development server, and watch it for changes. Run the command below to do that:

$ yarn run watch

Next, open a new terminal window/tab and run the PHP artisan command to serve the app:

$ php artisan serve

At this point, your Laravel application will be running on port 8000!

Building the Application Backend

Let’s start by creating API routes. In the backend, we’ll create two functionalities: The first is to generate a token, while the second is to create a channel.

To start with, create two endpoints by adding the following code to the ./routes/api.php file:

Route::post('generate_token','ChatMessagesController@generateToken');
Route::post('get_channel','ChatMessagesController@getChannel');

Let’s create the controller for this route!

ChatMessagesController

Run the command below to create a new controller called ChatMessagesController:

$ php artisan make:controller ChatMessagesController

Generating Your Token and Channel

Open ./app/Http/Controllers/ repository, then, copy the code below to the ChatMessagesController.php file:

<?php
namespace App\Http\Controllers;
use GetStream\StreamChat\Client;
use Illuminate\Http\Request;
class ChatMessagesController extends Controller
{
    protected $client;
    public function __construct(){
        $this->client =  new Client(
            getenv("MIX_STREAM_API_KEY"),
            getenv("MIX_STREAM_API_SECRET"),
        );
    }
    public function generateToken(Request $request){
        try{
            return response()->json([
                'token' => $this->client->createToken($request->name)
            ], 200);
        }catch(\Exception $e){
            return response()->json([
                'errorMessage' => $e->getMessage()
            ],500);
        }
    }
    public function getChannel(Request $request){
        try{
            $from = $request->from;
            $to = $request->to;
            $from_username = $request->from_username;
            $to_username = $request->to_username;
            $channel_name = "livechat-{$from_username}-{$to_username}";
            $channel = $this->client->getChannel("messaging", $channel_name);
            $channel->create($from_username, [$to_username]);
            return response()->json([
                'channel' => $channel_name
            ], 200);
        }catch(\Exception $e){
            return response()->json([
                'errorMessage' => $e->getMessage()
            ],500);
        }
    }
}

The above file consists of two functions and a constructor function. The constructor is used to instantiate Stream Chat. The function generateToken generates a token, while the function getChannel creates a new channel and returns the channel.

The Front End — The React Path

In the front end, we’ll create the admin chat functionality and the customer interface. We’ll send a message, listen to that message, and update the UI with the latest message.

This is where most of the work will be done. With that said, you can choose to move some of the jobs to the backend, if you like.

Let’s start with Routing!

Create a Wildcard Route

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

Replace the content of the ./route/web.php file with the code below:

<?php
    use Illuminate\Support\Facades\Route;
    Route::view('/{path?}', 'app');

The code above creates a wildcard route for our application. With this, our app will only return a view in - ./resources/views/app.blade.php. Next, create the app.blade file in the ./resources/views directory. Move the code below into app.blade file; this HTML excerpt is the first entry point template of the React app, where we’ll add chat bubbles:

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Stream Laravel React Live Chat</title>
        <!-- CSRF Token -->
        <meta name="csrf-token" content="{{ csrf_token() }}">
        <!-- Fonts -->
        <link href="https://fonts.googleapis.com/css?family=Nunito:200,600"
        rel="stylesheet">
        <!-- Styles -->
        <link href="{{ asset('css/app.css') }}" rel="stylesheet">
        <script
          src="https://kit.fontawesome.com/d82353c491.js"
          crossorigin="anonymous">
        </script>
    </head>
    <body>
        <div id="app"></div>
        <script src="{{ asset('js/app.js') }}"></script>
    </body>
</html>

Let’s proceed to create the App component!

Create the App Component

The App Component will be the base component of our React Components. So, we are renaming the Example.js file, located at ./resources/js/components/ (which is generated by Laravel) to App.js, then replace the code in that file with the code below:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Chat from './Chat/Chat';
import AdminChat from './Admin/AdminChat';
const App = () => {
    return (
        <BrowserRouter>
            <div>
                <Switch>
                    <Route exact path='/' component={Chat} />
                    <Route exact path='/admin' component={AdminChat} />
                </Switch>
            </div>
        </BrowserRouter>
    );
}

ReactDOM.render(<App />, document.getElementById('app'));

Then, to accommodate for this file name change, open ./resources/assets/js/app.js and replace

require('./components/Example');

with

require('./components/App');

In the App.js file, we are importing a client Chat component and an AdminChat component.

The reason for having two components is to distinguish between our chat screens (Client/Admin); we'll use React Router to route to the correct component, as needed. With that said, at this point, our application will throw an error if we try to access either, as we have not yet created either of these components. Let's dive into creating the Chat component!

Creating the Chat Component for the Client

First, we'll create the component for the client/user. Create a file called Chat.js in the directory ./resources/js/components/Chat/, and then copy the code below into that file:

import React, { useState, useEffect, useRef } from "react";
import { StreamChat } from 'stream-chat';
import './Chat.css';
import axios from 'axios';
const Chat = () => {
    return (
        <div className="row">
            <div className="container">
                <div className="row chat-window col-xs-5 col-md-3 p-0" id="chat_window_1">
                    <div className="col-xs-12 col-md-12 p-0">
                        <div className="panel panel-default">
                            <div className="panel-heading top-bar">
                                <div className="col-md-12 col-xs-12">
                                    <h3 className="panel-title"><span className="glyphicon glyphicon-comment"></span> Client Chat</h3>
                                </div>
                            </div>
                            <div className="panel-body msg_container_base">
                                <br/>

                                <div className="panel-footer">
                                    <div className="input-group">
                                        <input id="btn-input" type="text" className="form-control input-sm chat_input" placeholder="Write your message here..."  />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}
export default Chat;

To style this newly created component, we'll also create a file called Chat.css in the directory ./resources/js/components/Chat/, which will be populated with the following code:

body{
    height:400px;
    position: fixed;
    bottom: 0;
}
.col-md-2, .col-md-10{
    padding:0;
}
.panel{
    margin-bottom: 0px;
}
.chat-window{
    bottom:25px;
    position:fixed;
    right: 40px;
    box-shadow: 0px 0px 4px lightgrey;
}
.chat-window > div > .panel{
    border-radius: 5px 5px 0 0;
}
.icon_minim{
    padding:2px 10px;
}
.msg_container_base{
  background: rgb(239, 239, 239) 0px 3px 13px 0px;
  margin: 0;+
  padding: 0 10px 10px;
  max-height:300px;
  overflow-x:hidden;
}
.top-bar {
  background:rgb(0, 138, 255);
  color: white;
  padding: 10px;
  position: relative;
  overflow: hidden;
}
.msg_receive{
    padding-left:0;
    margin-left:0;
}
.msg_sent{
    padding-bottom:20px !important;
    margin-right:0;
}
.messages {
  background: white;
  padding: 10px;
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  max-width:100%;
}
.messages > p {
    font-size: 13px;
    margin: 0 0 0.2rem 0;
  }
.messages > time {
    font-size: 11px;
    color: #ccc;
}
.msg_container {
    padding: 10px;
    overflow: hidden;
    display: flex;
}
.base_sent {
  justify-content: flex-end;
  align-items: flex-end;
}
.base_sent > .avatar:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    width: 0;
    height: 0;
    border: 5px solid white;
    border-right-color: transparent;
    border-top-color: transparent;
    box-shadow: 1px 1px 2px rgba(black, 0.2);
}
.msg_sent > time{
    float: right;
}
.msg_container_base::-webkit-scrollbar-track
{
    box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
    background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar
{
    width: 12px;
    background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar-thumb
{
    box-shadow: inset 0 0 6px rgba(0,0,0,.3);
    background-color: #555;
}

Now, if you hit the URL "http://127.0.0.1:8000", you'll be able to view the page for user chat! The page should look like the picture below:

Client Chat

Creating the Chat Component for Admin

Next, we'll create the component for the admin! Create a file called AdminChat.js in the directory ./resources/js/components/Admin/ and copy in the code below, which renders the JSX for the Admin chat screen:

import React, { useState, useEffect, useRef } from "react";
import { StreamChat } from 'stream-chat';
import './AdminChat.css';
import axios from 'axios';
const AdminChat = () => {

    return (
        <div className="row">
            <div className="container">
                <div className="row chat-window col-xs-5 col-md-3 p-0" id="chat_window_1" style={{marginLeft:'10px'}}>
                    <div className="col-xs-12 col-md-12 p-0">
                        <div className="panel panel-default">
                            <div className="panel-heading admin-top-bar">
                                <div className="col-md-12 col-xs-12">
                                    <h3 className="panel-title"><span className="glyphicon glyphicon-comment"></span> Admin Chat</h3>
                                </div>
                            </div>
                            <div className="panel-body msg_container_base">
                                <br/>

                                <div className="panel-footer">
                                    <div className="input-group">
                                        <input id="btn-input" type="text" className="form-control input-sm chat_input" placeholder="Write your message here..." />
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}
export default AdminChat;

Just like for the client component, we'll go ahead and create a CSS style for AdminChat component. Create a file called AdminChat.css in the directory ./resources/js/components/Admin/ and add the code below:

body{
    height:400px;
    position: fixed;
    bottom: 0;
}
.col-md-2, .col-md-10{
    padding:0;
}
.panel{
    margin-bottom: 0px;
}
.chat-window{
    bottom:25px;
    position:fixed;
    right: 40px;
    box-shadow: 0px 0px 4px lightgrey;
}
.chat-window > div > .panel{
    border-radius: 5px 5px 0 0;
}
.icon_minim{
    padding:2px 10px;
}
.msg_container_base{
  background: rgb(239, 239, 239) 0px 3px 13px 0px;
  margin: 0;
  padding: 0 10px 10px;
  max-height:300px;
  overflow-x:hidden;
}
.admin-top-bar {
  background: rgb(0, 0, 0);;
  color: white;
  padding: 10px;
  position: relative;
  overflow: hidden;
}
.msg_receive{
    padding-left:0;
    margin-left:0;
}
.msg_sent{
    padding-bottom:20px !important;
    margin-right:0;
}
.messages {
  background: white;
  padding: 10px;
  border-radius: 2px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  max-width:100%;
}
.messages > p {
    font-size: 13px;
    margin: 0 0 0.2rem 0;
  }
.messages > time {
    font-size: 11px;
    color: #ccc;
}
.msg_container {
    padding: 10px;
    overflow: hidden;
    display: flex;
}
.base_sent {
  justify-content: flex-end;
  align-items: flex-end;
}
.base_sent > .avatar:after {
    content: "";
    position: absolute;
    bottom: 0;
    left: 0;
    width: 0;
    height: 0;
    border: 5px solid white;
    border-right-color: transparent;
    border-top-color: transparent;
    box-shadow: 1px 1px 2px rgba(black, 0.2);
}
.msg_sent > time{
    float: right;
}
.msg_container_base::-webkit-scrollbar-track
{
    box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
    background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar
{
    width: 12px;
    background-color: #F5F5F5;
}
.msg_container_base::-webkit-scrollbar-thumb
{
    box-shadow: inset 0 0 6px rgba(0,0,0,.3);
    background-color: #555;
}

Now, if you hit the URL "http://127.0.0.1:8000", you can view the page for admin chat! The page should look like the picture below:

Admin Chat Page

Chat Logic

Let's jump into the actual chatting logic for the client-side!

Add the code below to the Chat.js file located at ./resources/js/components/Chat/:

const [client, setClient] = useState(null);
const clientRef = useRef(null);
clientRef.current = client;

const [channel, setChannel] = useState(null);
const channelRef = useRef(null);
channelRef.current = channel;

const [message, setMessage]= useState('');
const messageRef = useRef('');
messageRef.current = message;

const [messageData, setMessageData]= useState([]);
const messageDataRef = useRef([]);
messageDataRef.current = messageData;

useEffect(() => {
    initializeClient();
    createChannel();
}, []);

const initializeClient = async () => {
    const {data} = await axios.post("api/generate_token",{name:'client'});
    const client = new StreamChat(process.env.MIX_STREAM_API_KEY, { timeout: 6000 });
    await client.setUser({id: 'client', name: 'Ukeh Hyginus'}, data.token);
    setClient(client);
}

const createChannel =  async () => {
    const {data} =  await axios.post('api/get_channel', {
        from_username: "client",
        to_username: "admin",
        from: "client",
        to: "admin",
    })
    if(clientRef.current && data){
        const channel = clientRef.current.channel('messaging', '', {
            name: 'LiveChat channel',
            members: ["client", "admin"]
        });

        channel.watch().then(state => {
            channel.on('message.new', event => {
                const messages = [...messageDataRef.current, event.message];
                setMessageData(messages);
            });
         })
        setChannel(channel);
    }
}

const getMessageHandler = (e) => {
    setMessage(e.target.value);
}

const sendMessageHandler = async (e) => {
    if(e.charCode === 13){
        if(channelRef.current){
            if(messageRef.current.trim().length > 0){
                const message = await channelRef.current.sendMessage({
                    text:messageRef.current
                });
                setMessage('');
            }
        }
    }
}

Then, to take care of the admin side, add the code below to the AdminChat.js file located at ./resources/js/components/Admin/:

const [client, setClient] = useState(null);
const clientRef = useRef(null);
clientRef.current = client;

const [channel, setChannel] = useState(null);
const channelRef = useRef(null);
channelRef.current = channel;

const [message, setMessage]= useState('');
const messageRef = useRef('');
messageRef.current = message;

const [messageData, setMessageData]= useState([]);
const messageDataRef = useRef([]);
messageDataRef.current = messageData;

useEffect(() => {
    initializeClient();
    createChannel();
}, []);

const initializeClient = async () => {
    const {data} = await axios.post("api/generate_token",{name:'admin'});

    const client = new StreamChat(process.env.MIX_STREAM_API_KEY, { timeout: 6000 });
    await client.setUser({id: 'admin', name: 'Eze Sunday'}, data.token);
    setClient(client);
}
const createChannel =  async () => {
    const {data} =  await axios.post('api/get_channel', {
        from_username: "admin",
        to_username: "client",
        from: "admin",
        to: "client",
    })
    if(clientRef.current && data){
        const channel = clientRef.current.channel('messaging', '', {
            name: 'LiveChat channel',
            members: ["admin", "client"]
        });

        channel.watch().then(state => {
            channel.on('message.new', event => {
                const messages = [...messageDataRef.current, event.message];
                setMessageData(messages);
            });
        })
        setChannel(channel);
    }
}

const getMessageHandler = (e) => {
    setMessage(e.target.value);
}

const sendMessageHandler = async (e) => {
    if(e.charCode === 13){
        if(channelRef.current){
            if(messageRef.current.trim().length > 0){
                const message = await channelRef.current.sendMessage({
                    text:messageRef.current
                });
                setMessage('');
            }
        }
    }
}

Now, let’s review the methods we used.

useState() - We use hooks to manage State in our application. Here we used useState and useRef to maintain the state of the client, channel, message and messageData:

    const [client, setClient] = useState(null);
    const clientRef = useRef(null);
    clientRef.current = client;

    const [channel, setChannel] = useState(null);
    const channelRef = useRef(null);
    channelRef.current = channel;

    const [message, setMessage]= useState('');
    const messageRef = useRef('');
    messageRef.current = message;

    const [messageData, setMessageData]= useState([]);
    const messageDataRef = useRef([]);
    messageDataRef.current = messageData;

useEffect() - We use the React useEffect hook to run the functions initializeClient and createChannel. Because the initializationClient method will make a network call and get the data that our app requires to properly render the component, it makes sense that we execute it in the useEffect hook:

useEffect(() => {
  initializeClient();
  createChannel();
}, []);

initializeClient() - This method calls the API endpoint to generate a token for the client. It then initializes StreamChat, sets the user and verifies the token before updating the state with the client object:

const initializeClient = async () => {
    const {data} = await axios.post("api/generate_token",{name:'admin'});

    const client = new StreamChat(process.env.MIX_STREAM_API_KEY, { timeout: 6000 });
    await client.setUser({id: 'admin', name: 'Eze Sunday'}, data.token);
    setClient(client);
}

createChannel() - This method calls our endpoint to create a channel and then passes to required data to the channel instance, and, finally, watches the channel for any new messages. If there is a new message, it will update the state with the new message data.

The most interesting part about creating a live chat application with Stream is that you don’t need to build one-one chat any differently than you would a group chat; you just need to set the members of the channel to two (2) users and the channel title to an empty string, and, boom, you have a one to one chat:

const channel = clientRef.current.channel('messaging', '', {
  name: 'LiveChat channel',
  members: ['admin', 'client'],
});

We’ll hardcode the names of the members (admin and client) for the sake of this tutorial.

getMessageHandler() - This method gets the message entered from the textbox and then updates our state with it:

const getMessageHandler = (e) => {
  setMessage(e.target.value);
};

SendMessageHander - Finally, we'll send the message to Stream, if the message is not an empty string:

const sendMessageHandler = async (e) => {
    if(e.charCode === 13){
        if(channelRef.current){
            if(messageRef.current.trim().length > 0){
                const message = await channelRef.current.sendMessage({
                    text:messageRef.current
                });
                setMessage('');
            }
        }
    }
}

We'll then listen to the message:

channel.watch().then(state => {
    channel.on('message.new', event => {
        const messages = [...messageDataRef.current, event.message];
        setMessageData(messages);
    });
})

And push the new message to the UI.

Rendering the Chats Dynamically

When the event listener gets the message, we push the code to the UI using the bubble template. Add the following code to the Chat.js file located at ./resources/js/components/Chat/. This should be added immediately after the
tag inside the

tag with class name panel-body msg_container_base:

{messageData.map((message)=>{
return(
  <div key={message.id} className={`row msg_container ${message.user.id == 'client' ? 'base_sent': 'base_receive'}`}>
  <div className="col-md-10 col-xs-10">
      <div className="messages msg_sent">
          <p>{message.text}</p>
          <time dateTime={message.created_at}>{message.user.name}</time>
      </div>
  </div>
  </div>
  );
})}

You should do the same to the Admin side (./resources/js/components/Admin/AdminChat.js file):

{messageData.map((message)=>{
  return(
      <div key={message.id} className={`row msg_container ${message.user.id == 'admin' ? 'base_sent': 'base_receive'}`}>
          <div className="col-md-10 col-xs-10">
              <div className="messages msg_sent">
                  <p>{message.text}</p>
                  <time dateTime={message.created_at}>{message.user.name}</time>
              </div>
          </div>
      </div>
  );
})}

The difference between the admin and the client is that, in the admin part, we send "admin" as the logged-in user, while we set the "client" as the logged-in user on the client-side.

Congratulations 👍, your app should be up and running!

Hit the URL http://localhost:8000 to start chatting as a client and then visit the Admin URL (http://localhost:8000/admin) to respond to the client’s messages.

Wrapping Up

Cheers, I can’t wait to see the amazing applications you’ll build with Stream. Check out Stream Chat and the associated docs to learn more about Stream and the API/SDK!

Thanks for reading and 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 ->