💬 Building a real-time chat with Websockets, Novel and Clerk 🚀🚀

Nevo David - Sep 7 '23 - - Dev Community

TL;DR

In this tutorial, you'll learn how to build a chat application.

On the agenda 🔥:

  • Create accounts and send real-time messages using React.js, Node.js and Websockets.
  • Build an authentication with Clerk.
  • Add a rich-text editor for the chat with Novel.

Chatting


Novu: Open-source notification infrastructure 🚀

Just a quick background about us. Novu is an open-source notification infrastructure. We basically help to manage all the product notifications. It can be In-App (the bell icon like you have in the Dev Community), Emails, SMSs and so on.

Like

https://github.com/novuhq/novu


Let's set it up 🚀

Socket.io is a popular JavaScript library that allows us to create real-time, bi-directional communication between web browsers and a Node.js server. It is a highly performant and reliable library optimised to process a large volume of data with minimal delay.

Here, you'll learn how to add Socket.io to a React and Node.js application and connect both development servers for real-time communication.

Create a folder for the web application as done below.



mkdir chat-app
cd chat-app
mkdir client server


Enter fullscreen mode Exit fullscreen mode

Navigate into the client folder via your terminal and create a new React.js project with Vite.



npm create vite@latest


Enter fullscreen mode Exit fullscreen mode

Install Socket.io client API and React Router. React Router is a JavaScript library that enables us to navigate between pages in a React application.



npm install socket.io-client react-router-dom


Enter fullscreen mode Exit fullscreen mode

Delete the redundant files such as the logo and the test files from the React app, and update the App.jsx file to display “Hello World” as below.



function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}


Enter fullscreen mode Exit fullscreen mode

Copy the CSS file required for styling the project into the src/index.css file.

Connecting the React app to the Node.js server

Run the code snippet below to create a package.json file within the server folder.



cd server
npm init -y


Enter fullscreen mode Exit fullscreen mode

Install Express.js, CORS, Nodemon, and Socket.io Server API.

Express.js is a fast, minimalist framework that provides several features for building web applications in Node.js. CORS is a Node.js package that allows communication between different domains.

Nodemon is a Node.js tool that automatically restarts the server after detecting file changes, and Socket.io allows us to configure a real-time connection on the server.



npm install express cors nodemon socket.io


Enter fullscreen mode Exit fullscreen mode

Create an index.js file - the entry point to the web server.



touch index.js


Enter fullscreen mode Exit fullscreen mode

Set up a simple Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.



//👇🏻 index.js
const express = require("express");
const app = express();
const PORT = 4000;

app.get("/api", (req, res) => {
    res.json({
        message: "Hello world",
    });
});

app.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});


Enter fullscreen mode Exit fullscreen mode

Import the HTTP and the CORS library to allow data transfer between the client and the server domains.



const express = require("express");
const app = express();
const PORT = 4000;

//👇🏻 New imports
const http = require("http").Server(app);
const cors = require("cors");

app.use(cors());

app.get("/api", (req, res) => {
    res.json({
        message: "Hello world",
    });
});

http.listen(PORT, () => {
    console.log(`Server listening on ${PORT}`);
});


Enter fullscreen mode Exit fullscreen mode

Next, add Socket.io to the project to create a real-time connection. Before the app.get() block, copy the code below.



//👇🏻 New imports
const socketIO = require("socket.io")(http, {
    cors: {
        origin: "http://localhost:5173",
    },
});

//👇🏻 Add this before the app.get() block
socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);
    socket.on("disconnect", () => {
        console.log("🔥: A user disconnected");
    });
});


Enter fullscreen mode Exit fullscreen mode

From the code snippet above, the socket.io("connection") function establishes a connection with the React app, creates a unique ID for each socket, and logs the ID to the console whenever a user visits the web page.

When you refresh or close the web page, the socket fires the disconnect event to show that a user has disconnected from the socket.

Next, configure Nodemon by adding the start command to the list of the scripts in the package.json file. The code snippet below starts the server using Nodemon.



//👇🏻In server/package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js"
  },


Enter fullscreen mode Exit fullscreen mode

You can now run the server with Nodemon by using the command below.



npm start


Enter fullscreen mode Exit fullscreen mode

Open the App.jsx file in the client folder and connect the React app to the Socket.io server.



import socketIO from "socket.io-client";
const socket = socketIO.connect("http://localhost:4000");

function App() {
    return (
        <div>
            <p>Hello World!</p>
        </div>
    );
}


Enter fullscreen mode Exit fullscreen mode

Start the React.js server by running the code snippet below.



npm run dev


Enter fullscreen mode Exit fullscreen mode

Check the server's terminal; the ID of the React.js client will be displayed. Congratulations 🥂 , you've successfully connected the React app to the server via Socket.io.


Adding authentication to your app 👤

Clerk is a complete user management package that enables you to add various forms of authentication to your software applications. With Clerk, you can authenticate users via password and password-less sign-in, social account login, SMS verification, and Web3 authentication.

Clerk also provides prebuilt authentication components that enable you to authenticate users easily and focus more on the application's logic. These components are also customisable.

In this article, I'll walk you through

  • adding Clerk to a React app,
  • authenticating users with Clerk,
  • sending real-time messages with Socket.io, and
  • adding novel text editor to a React application.

Adding Clerk to your App

Here, you'll learn how to authenticate users via Clerk. Before we proceed, create a Clerk account.

Create a new Clerk application, as shown below.

clerk

Copy your Publishable key into a .env file within your React app.



VITE_REACT_APP_CLERK_PUBLISHABLE_KEY=<your_publishable_key>


Enter fullscreen mode Exit fullscreen mode

Publishable

Finally, update the App.jsx file to display the Signup and Signin UI components provided by Clerk.



import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import Home from "./components/Home";
import socketIO from "socket.io-client";
import {
    ClerkProvider,
    SignedIn,
    SignedOut,
    SignIn,
    SignUp,
    RedirectToSignIn,
} from "@clerk/clerk-react";

//👇🏻 gets the publishable key
const clerkPubKey = import.meta.env.VITE_REACT_APP_CLERK_PUBLISHABLE_KEY;

//👇🏻 socketIO configuration
const socket = socketIO.connect("http://localhost:4000");

const App = () => {
    return (
        <Router>
            <ClerkProvider publishableKey={clerkPubKey}>
                <Routes>
                    <Route
                        path='/*'
                        element={
                            <div className='login'>
                                <SignIn
                                    path='/'
                                    routing='path'
                                    signUpUrl='/register'
                                    afterSignInUrl='/chat'
                                />{" "}
                            </div>
                        }
                    />

                    <Route
                        path='/register/*'
                        element={
                            <div className='login'>
                                <SignUp afterSignUpUrl='/chat' />
                            </div>
                        }
                    />

                    <Route
                        path='/chat'
                        element={
                            <>
                                <SignedIn>
                                    <Home socket={socket} />
                                </SignedIn>
                                <SignedOut>
                                    <RedirectToSignIn />
                                </SignedOut>
                            </>
                        }
                    />
                </Routes>
            </ClerkProvider>
        </Router>
    );
};

export default App;


Enter fullscreen mode Exit fullscreen mode
  • From the code snippet above,
    • You need to wrap the entire application with the ClerkProvider component and pass the publishable key as a prop into the component.
    • The SignIn and SignUp components enable you to add Clerk's authentication components to your React app.
    • The Home component is a protected route which is only available to authenticated users.

Clerk

Create a components folder containing the Home.jsx file. This will be homepage for the chat application.



cd client/src
mkdir components
touch Home.jsx


Enter fullscreen mode Exit fullscreen mode

Home

Copy the code below into the Home.jsx file to replicate the chat UI above.



import { useState } from "react";
import { Link } from "react-router-dom";
import { SignOutButton, useAuth } from "@clerk/clerk-react";

const Home = ({ socket }) => {
    const { isLoaded, userId } = useAuth();
    const [write, setWrite] = useState(false);
    const writeFunction = () => setWrite(true);

    const handleSubmit = () => {
        console.log({ message: "Submit Clicked!", userId });
        setWrite(false);
    };

    // In case the user signs out while on the page.
    if (!isLoaded || !userId) {
        return null;
    }

    return (
        <div>
            <nav className='navbar'>
                <Link to='/' className='logo'>
                    Mingle
                </Link>
                <SignOutButton signOutCallback={() => console.log("Signed out!")}>
                    <button className='signOutBtn'>Sign out</button>
                </SignOutButton>
            </nav>

            {!write ? (
                <main className='chat'>
                    <div className='chat__body'>
                        <div className='chat__content'>
                            {/**-- contains chat messages-- */}
                        </div>
                        <div className='chat__input'>
                            <div className='chat__form'>
                                <button className='createBtn' onClick={writeFunction}>
                                    Write message
                                </button>
                            </div>
                        </div>
                    </div>
                    <aside className='chat__bar'>
                        <h3>Active users</h3>
                        <ul>
                            <li>David</li>
                            <li>Dima</li>
                        </ul>
                    </aside>
                </main>
            ) : (
                <main className='editor'>
                    <header className='editor__header'>
                        <button className=' editorBtn' onClick={handleSubmit}>
                            SEND MESSAGE
                        </button>
                    </header>

                    <div className='editor__container'>Your editor container</div>
                </main>
            )}
        </div>
    );
};

export default Home;


Enter fullscreen mode Exit fullscreen mode

From the code snippet above, Clerk provides a SignOutButton component and a useAuth hook. The SignOutButton component can be wrapped around a custom button tag, and the useAuth hook enables us to access the current user's ID. We'll use the user's ID for identifying users on the Node.js server.

When users click the "Write message" button, the UI changes to the Editor screen. In the upcoming section, you'll learn how to add the Novel text editor to a React app.

Write a message


Adding the next-gen editor to our chat 💬

Novel is a Notion-style WYSIWYG editor that supports various text formats and image upload. It also provides AI auto-completion.

Install Novel by running the code snippet below.



npm install novel


Enter fullscreen mode Exit fullscreen mode

Import the Editor component into the Home.jsx component.



import { Editor } from "novel";
import "novel/styles.css";


Enter fullscreen mode Exit fullscreen mode

Add the Editor component to the UI as shown below.

Add the Editor component to the UI as shown below.



<div>
        <main className='editor'>
            <header className='editor__header'>
                <button className=' editorBtn' onClick={handleSubmit}>
                    SEND MESSAGE
                </button>
            </header>

            <div className='editor__container'>
                {/**-- 👇🏻 Editor component --**/}
                <Editor onUpdate={(e) => setValue(updateMessage(e.content))} />
            </div>
        </main>
</div>


Enter fullscreen mode Exit fullscreen mode

Add the code snippet below to the component to access the user’s input.



//👇🏻 holds the Editor's content
const [value, setValue] = useState([]);

//👇🏻 saves only the heading and paragraph texts
const updateMessage = (array) => {
    const elements = [];
    for (let i = 0; i < array.length; i++) {
        if (array[i].type === "paragraph" || array[i].type === "heading") {
            elements.push(array[i].content[0].text);
        }
    }
    return elements.join("\n");
};


Enter fullscreen mode Exit fullscreen mode

Handle real-time communication 👨‍👨‍👧

Here, you'll learn how to send the user's messages to the Node.js server and also view online users.

Create a handleSubmit function that sends the Editor's content and the user's ID to the server when the user clicks the Send Message button.



const handleSubmit = () => {
    socket.emit("message", {
        value,
        userId: userId.slice(0, 10),
    });
    setWrite(false);
};


Enter fullscreen mode Exit fullscreen mode

Update the Socket.io listener on the Node.js server to listen to the message event.



socketIO.on("connection", (socket) => {
    console.log(`⚡: ${socket.id} user just connected!`);
    //👇🏻 receives the data from the React app
    socket.on("message", (data) => {
        console.log(data);
        socketIO.emit("messageResponse", data);
    });

    socket.on("disconnect", () => {
        console.log("🔥: A user disconnected");
    });
});


Enter fullscreen mode Exit fullscreen mode

The code snippet above receives the data from the message event and sends the message back to the React app to display its content.

Add the event listener to the React app.



//👇🏻 holds online users
const [onlineUsers, setOnlineUsers] = useState([]);
//👇🏻 holds all the messages
const [messages, setMessages] = useState([]);

useEffect(() => {
    socket.on("messageResponse", (data) => {
        setMessages([...messages, data]);
        if (!onlineUsers.includes(data.userId)) {
            setOnlineUsers([...onlineUsers, data.userId]);
        }
    });
}, [socket, messages, onlineUsers]);


Enter fullscreen mode Exit fullscreen mode

The code snippet above adds the new message to the messages array and updates the onlineUsers state if the user's ID is not on the list.

Scroll down on a new message 🆕

In this section, you'll learn how to move the scrollbar to the most recent message when there is a new message.

Scroll

Create a new ref with the useRef hook that scrolls the bottom of the messages container.



import { useRef, useEffect } from "react";
const lastMessageRef = useRef(null);

useEffect(() => {
    // 👇️ scroll to bottom every time messages change
    lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);


Enter fullscreen mode Exit fullscreen mode

Add a div element with a ref attribute, as shown below.



<div>
    {/** --- messages container ---*/}
    <div ref={lastMessageRef} />
</div>


Enter fullscreen mode Exit fullscreen mode

When there is a new message, the scrollbar focuses on the latest message.

Congratulations!🎉 You've completed this project.


Conclusion

So far, you've learnt how to authenticate users with Clerk, send real-time messages via Socket.io in a React and Node.js application, and add Novel WYSIWYG editor to a React app.

Socket.io is an excellent tool for building efficient applications that require real-time communication, and Clerk is a great authentication management system that provides all forms of authentication. It is also open-source - you can request customized features or contribute to the tool as a developer.

The source code for this tutorial is available here:

https://github.com/novuhq/blog/tree/main/chat-app-with-websockets-novel

Thank you for reading!


Like

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .