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.
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.
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
Navigate into the client folder via your terminal and create a new React.js project withย Vite.
npm create vite@latest
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
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>
);
}
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
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
Create an index.js
file - the entry point to the web server.
touch index.js
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}`);
});
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}`);
});
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");
});
});
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"
},
You can now run the server with Nodemon by using the command below.
npm start
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>
);
}
Start the React.js server by running the code snippet below.
npm run dev
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.
Copy your Publishable key into a .env
file within your React app.
VITE_REACT_APP_CLERK_PUBLISHABLE_KEY=<your_publishable_key>
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;
- 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.
- You need to wrap the entire application with the
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
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;
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.
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
Import the Editor component into the Home.jsx
component.
import { Editor } from "novel";
import "novel/styles.css";
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>
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");
};
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);
};
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");
});
});
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]);
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.
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]);
Add a div element with a ref attribute, as shown below.
<div>
{/** --- messages container ---*/}
<div ref={lastMessageRef} />
</div>
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!
Top comments (17)
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";
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.
Novel is cool! I haven't heard of it before :) We have to try build something for @wasplang with it :)
It's awesome! maintained by @steventey
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.
Really loved the idea of
Markdown based Chat ๐
Also, thanks a lot for the detailed Explanation. Will try to build on this idea ๐ก
Just putting it out there, for the realtime environment you can use NoLag. No need to build your own websocket server. ๐
Great post! It's well-written ๐
Thank you so much Kiran!
Great post!
very very very good project.
Great article, keep sharing with the community