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!