Build a powerful chat application using React Hooks

Akash, author for building powerful chat blog
Akash Joshi
A fullstack developer & open source contributor who finds joy in building products

In our article on CSS-Tricks, we built a fully functional chat application in an easy, secure, and fast way using React Hooks. You would think (and rightly so) that achieving this is no mean feat. Yes, that is how powerful React Hooks are!

In the article, we will take our chat app to the next level by adding some nifty features using a few more hooks. We will add the ability to show whether the user is online or offline, add local storage capabilities and clipboard functionality. Our chat app elements will also adapt to the current window size.

Perfect! Let’s dive right into building our chat app then.

Hooks-Chat-2_16X9-4-ue7i3

Table of Contents

  • App features
  • Building the App
    1. 1. use-localstorage
    2. 2. useOnlineStatus
    3. 3. useWindowSize
    4. 4. useClippy
    5. 5. Finishing the app
  • What next?

Important Note – This article assumes you already have a working chat app as described in this article. Please go through the article, if you haven’t already done so, before proceeding further.

If you are looking at an introductory article on React Hooks, refer this one where we build a note taking app.


App features

The chat application will now have the following additional features:

  • Local storage of userid, room, allowing persistence of session.
  • Displaying whether the user is Online or Offline.
  • Displaying different elements depending on screen size.
  • Logging out of application when required.

Check out a glimpse of the completed app below:

Here is how it will look on a mobile device:


Building the App

1. use-localstorage

About the hook

We will have use-localstorage replace the useState hooks used previously to give persistence to user login. It allows us to use our local storage as a variable store in our application. Data that remains constant, such as username, room which the user is connected to, etc. is stored in localStorage, so that it persists between user sessions.

Advantage

If we want our app to have local storage capabilities and if we don’t intend to use this hook implies we would have to deal with the lower level localStorage API. LocalStorage API can’t be used directly as well since it requires type checking and lifecycle hooks to be used correctly. use-localstorage hook does that automatically for us, and so, we only need to consume the hook endpoint to use local storage.

Implementation

First, install the hook by running command

npm add react-use-localstorage

The hook is used as follows:

const [data, setData] = useLocalStorage('storage_id', default_value)

To use this hook, we simply replace our previous room & id useState hooks with this one.

...
import React, { useEffect } from 'react';
import useLocalStorage from 'react-use-localstorage';
...
...
const [room, setRoom] = useLocalStorage('room','');
const [id, setId] = useLocalStorage('id','');
...

Add this so that React logs back in if an id is present:

...
useEffect(()=>{
    if(id !== ''){
      socket.emit("join", id, room);
    }
...

This simple change makes our app state remain the same between reloads


2. useOnlineStatus

About the hook

We use the useOnlineStatus hook to show the user when (s)he is online or offline. The hook listens to the window event listener and automatically returns true or false, depending on the current connectivity status.

Advantage

Checking for online status does not have a standardised API & each browser implements it differently. So, using useOnlineStatus gives us an assured way of knowing whether the user is online without writing multiple lines of code.

Implementation

Install the hook by running

npm add @withvoid/melting-pot

The hook is used as follows:

const { online } = useOnlineStatus();

with the “online” variable returning true or false depending on network status.

We will now import the hook, define it in our component, and return ‘You are Online’ or ‘You are Offline’ based on network status.

...
import { useImmer } from 'use-immer';
import { useOnlineStatus } from '@withvoid/melting-pot';
...
...
// IMPORTANT - Rename our previous online variable to onlineList
const [onlineList, setOnline] = useImmer([]);
const { online } = useOnlineStatus();
...
...
<ul id="mess
ages"><Messages data={messages} /></ul>
<ul id="online"> {online ? '❤️ You are Online' : '💛 You are Offline'} <hr/><Online data={onlineList} /> </ul>
...


Join our Network of Top React Engineers and Work with Top Startups & Companies!


3. useWindowSize

About the hook

We use the useWindowSize hook to display different elements according to current width of browser window. It gets this value from the window event listener.

Advantage

Although getting window size does have a standard API, this hook allows us to skip all the boiler-plate and get the value instantly in the hook format.

Implementation

This hook is imported from the same library from which we imported the useOnlineStatus hook. So, if you have already installed it, move on to using the hook, otherwise, install the library by running

npm add @withvoid/melting-pot

The hook is used as follows:

const { width } = useWindowSize();

with the width hook automatically re-rendering our component when it changes.

The code we add will display ‘Send Message’ or an emoji, based on current width of screen.

Add this to the tag of HTML

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Add this to App.js

...
import { useOnlineStatus, useWindowSize } from '@withvoid/melting-pot';
...
...
const { online } = useOnlineStatus();
const { width } = useWindowSize();
...
...
<form onSubmit={e => handleSend(e)} style={{display: 'flex'}}>
    <input id="m" />
    {width > 1000 ? <button style={{width:'100px'}} type="submit">Send Message</button> :
  <button style={{width:'50px'}}><i style={{fontSize:'15px'}} class="material-icons">send</i></button>}
...
Hooks-Screenshot_2019-05-08_at_6.02.18_PM-grprn
Hooks_Mobile-1wvke

4. useClippy

About the hook

useClippy will allow our app to have the “Add to clipboard” functionality. It enables users to instantly copy a piece of content to clipboard.

Advantage

Even though implementing copy to clipboard functionality is moderately easy, this hook allows you complete read & write control over your clipboard in only a few lines of code.

Implementation

First, install the hook

npm add use-clippy

This hook is used as follows:

const [ clipboard, setClipboard ] = useClippy();

with clipboard containing current copied text, and setClipboard setting its value.

import useClippy from 'use-clippy';

Add an icon to each message by updating the Messages component with this

<i style={{float:'right',color:'black'}} class=" material-icons">content_copy</i>

Add a onClick function to the icon so that it automatically copies the message content to clipboard when clicked.

<a onClick={()=>{setClipboard(`${m[1]}`)}} href="#"><i style={{float:'right',color:'black'}} class=" material-icons">content_copy</i></a> <div className="innermsg">{m[1]}</div></li>) : (<li className="update">{m[1]}</li>) );

The final component code will look like this

const Messages = props => { 
  const [ clipboard, setClipboard ] = useClippy();

  return props.data.map(m => m[0] !== '' ? (<li><strong>{m[0]}</strong> :
  <a onClick={()=>{setClipboard(`${m[1]}`)}} href="#"><i style={{float:'right',color:'black'}} class=" material-icons">content_copy</i></a> <div className="innermsg">{m[1]}</div></li>) : (<li className="update">{m[1]}</li>) ); 
}
Hooks-Screenshot_2019-05-10_at_5.58.35_PM-061e9

5. Finishing the app

As a final touch, we will add logging out functionality in our application.

We will simply set the id value as empty whenever the user clicks the cross icon We will also disconnect the socket from server so that server gets the disconnection message from client. You can check out the “logOut” code in the file below.

import React, { useEffect } from 'react';
import useLocalStorage from 'react-use-localstorage';
import useSocket from 'use-socket.io-client';
import { useImmer } from 'use-immer';
import { useOnlineStatus, useWindowSize } from '@withvoid/melting-pot';
import useClippy from 'use-clippy';

import './index.css';

const Messages = props => { 
  const [ clipboard, setClipboard ] = useClippy();

  return props.data.map(m => m[0] !== '' ? 
(<li><strong>{m[0]}</strong> :<a onClick={()=>{setClipboard(`${m[1]}`)}} href="#"><i style={{float:'right',color:'black'}} class=" material-icons">content_copy</i></a> <div className="innermsg">{m[1]}</div></li>) 
: (<li className="update">{m[1]}</li>) ); 
}

const Online = props => props.data.map(m => <li id={m[0]}>{m[1]}</li>)

export default () => {
  const [room, setRoom] = useLocalStorage('room','');
  const [id, setId] = useLocalStorage('id', '');

  const [socket] = useSocket('https://open-chat-naostsaecf.now.sh');

  const [messages, setMessages] = useImmer([]);

  const [onlineList, setOnline] = useImmer([]);

  const { online } = useOnlineStatus();
  const { width } = useWindowSize();

  useEffect(()=>{
    socket.connect();

    if(id){
      socket.emit('join',id,room);
    }

    socket.on('message que',(nick,message) => {
      setMessages(draft => {
        draft.push([nick,message])
      })
    });

    socket.on('update',message => setMessages(draft => {
      draft.push(['',message]);
    }))

    socket.on('people-list',people => {
      let newState = [];
      for(let person in people){
        newState.push([people[person].id,people[person].nick]);
      }
      setOnline(draft=>{draft.push(...newState)});
    });

    socket.on('add-person',(nick,id)=>{
      setOnline(draft => {
        draft.push([id,nick])
      })
    })

    socket.on('remove-person',id=>{
      setOnline(draft => draft.filter(m => m[0] !== id))
    })

    socket.on('chat message',(nick,message)=>{
      setMessages(draft => {draft.push([nick,message])})
    })
  },0);

  const handleSubmit = e => {
    e.preventDefault();
    const name = document.querySelector('#name').value.trim();
    const room_value = document.querySelector('#room').value.trim();
    console.log(name);
    if (!name) {
      return alert("Name can't be empty");
    }
    setId(name);
    setRoom(room_value);
    socket.emit("join", name,room_value);
  };

  const handleSend = e => {
    e.preventDefault();
    const input = document.querySelector('#m');
    if(input.value.trim() !== ''){
      socket.emit('chat message',input.value,room);
      input.value = '';
    }
  }

  const logOut = () => {
    socket.disconnect();
    setOnline(draft=>[]);
    setMessages(draft=>[]);
    setId('');
    socket.connect();
  }

  return id !== '' ? (
    <section style={{display:'flex',flexDirection:'row'}} >
      <ul id="messages"><Messages data={messages} /></ul>
      <ul id="online"> <a onClick={()=>logOut()} href='#'><div style={{float:'right'}}>❌</div></a> {online ? '❤️ You are Online' : '💛 You are Offline'} <hr/><Online data={onlineList} /> </ul>
      <div id="sendform">
        <form onSubmit={e => handleSend(e)} style={{display: 'flex'}}>
            <input id="m" />
            {width > 1000 ? <button style={{width:'100px'}} type="submit">Send Message</button> :
          <button style={{width:'50px'}}><i style={{fontSize:'15px'}} class="material-icons">send</i></button>}
        </form>
      </div>
    </section>
  ) : (
    <div style={{ textAlign: 'center', margin: '30vh auto', width: '70%' }}>
      <form onSubmit={event => handleSubmit(event)}>
        <input id="name" required placeholder="What is your name .." /><br />
        <input id="room" placeholder="What is your room .." /><br />
        <button type="submit">Submit</button>
      </form>
    </div>
  );
};

Congratulations! Your new chat app is now much more powerful!

The complete code for the project can be found here: https://github.com/akash-joshi/socket-blog-client.


What next?

You can now use your newly gained knowledge to make a lot of apps. Here are a few ideas to get you started:

  • Blogging Application
  • Instagram Clone
  • Reddit Clone
  • Beautiful Single Page Website

Happy coding!