DEV Community

Cover image for ๐Ÿ’ฌ Building a real-time chat with Websockets, Novel and Clerk ๐Ÿš€๐Ÿš€
Nevo David Subscriber for novu

Posted on

๐Ÿ’ฌ Building a real-time chat with Websockets, Novel and Clerk ๐Ÿš€๐Ÿš€

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

Top comments (17)

Collapse
 
mitchleung profile image
Mitch

FYI

The latest version of "@clerk/clerk-react": "^4.24.2" tends to put me an infinite loop

"novel": "^0.1.20" doesn't have css import "novel/styles.css";

Collapse
 
techelder1642 profile image
Robert L Carter • Edited

Great project! But I believe I have ran into an issue after signing up an user on the chat. It did this refresh loop until I stopped the server or when I deleted the user from clerk. I cannot seem to find a way to rectify it either.

Could we chat a bit about when you have the chance?

Edit: After some more debugging , it seems that this issue has popped up since clerk ver 4.24.2. Now I like to figure out what was added to is now causing this.

Collapse
 
matijasos profile image
Matija Sosic

Novel is cool! I haven't heard of it before :) We have to try build something for @wasplang with it :)

Collapse
 
nevodavid profile image
Nevo David

It's awesome! maintained by @steventey

Collapse
 
daniel43532 profile image
Daniel43532

Building a real-time chat application with Websockets, Novel, and Clerk sounds like an exciting project! Here's a high-level overview of how you can approach it:

Setup Your Development Environment:

Make sure you have Node.js installed on your system.
Set up a new project directory areej shah novels.
Initialize Your Project:

Use npm or yarn to initialize a new Node.js project.
Install necessary dependencies like Express.js, Socket.io (for Websockets), Novel, and Clerk.

Collapse
 
thevinitgupta profile image
Vinit Gupta

Really loved the idea of

Markdown based Chat ๐Ÿš€

Also, thanks a lot for the detailed Explanation. Will try to build on this idea ๐Ÿ’ก

Collapse
 
hencoburger profile image
HencoBurger

Just putting it out there, for the realtime environment you can use NoLag. No need to build your own websocket server. ๐Ÿ˜

Collapse
 
devkiran profile image
Kiran Krishnan

Great post! It's well-written ๐Ÿ‘

Collapse
 
nevodavid profile image
Nevo David

Thank you so much Kiran!

Collapse
 
samucadev profile image
SamucaDev

Great post!

Collapse
 
muhyilmaz profile image
MUHAMMED YILMAZ

very very very good project.

Collapse
 
respect17 profile image
Kudzai Murimi

Great article, keep sharing with the community