How to Use WebSockets in Next.js to Create a Chat App

Raazesh Prajapati
readytowork, Inc.
Published in
9 min readApr 11, 2023
fig: Next-js with WebSocket

If you ever used the Messenger app in its early days, you may have noticed that we had to frequently refresh the page to check whether a new message had arrived or not. However, now we can see messages as soon as someone sends them to us, without having to refresh the page. This is possible due to the emergence of a technology called WebSockets.

🤔 What are WebSockets?

fig: WebSocket protocol

WebSockets are a protocol that provides a bidirectional, full-duplex communication channel over a single TCP connection between a client and a server. It is widely used in web applications that require real-time communication, such as chat applications, online gaming, and financial trading platforms. It is supported by all modern web browsers, as well as many server-side programming languages and frameworks.

💻 Creating chat application:

In this article, we will implement WebSockets in our front-end(client) application and create a small chat app using next-js (version 13).

In this article, we are just covering front-end implementation. The backend implementation is been covered by my friend Damodar Bastakoti in his article WebSockets in go using gorilla.

🌟Getting Started

Before starting our project we should have the following prerequisites in our local system.

📦Prerequisites:

⚙️ Creating and setting up Next-js project:

We are using create-next-app, which sets up everything automatically for us. We want to start with a TypeScript in our project so we should use the --typescript flag. To create a project, run:

# I am using yarn as package manager
yarn create next-app --typescript
# or
npx create-next-app@latest --typescript
# or
pnpm create next-app --typescript

For more about NextJs application, you can visit next-js official documentation here.

For good UI I am using ant-design and Styled-component for styling. So lets us install and set them up.

yarn add antd styled-components

After all these setups hope our project folder tree looks something like this:

.
├─ .eslintrc.json
├─ .git
├─ .gitignore
├─ .next
├─ README.md
├─ next.config.js
├─ node_modules
├─ package-lock.json
├─ package.json
├─ public
├─ src
│ ├─ pages
│ │ ├─ _app.tsx
│ │ ├─ _document.tsx
│ │ └─ index.tsx
│ └─ styles
│ └─ globals.css
├─ tsconfig.json
└─ yarn.lock

Since we are using a package manager yarn that supports the “resolutions” package.json field, we need to add an entry to it as well corresponding to the major version range. This helps avoid an entire class of problems that arise from multiple versions of styled components being installed in your project.

In package.json:

{
"resolutions": {
"styled-components": "^5"
}
}

Since NextJs13 creates components and pages server side, so we have to configure _document.tsx to work Styled component properly.

import Document, { Html, Head, Main, NextScript } from "next/document";
import { ServerStyleSheet } from "styled-components";

export default class MyDocument extends Document {
static async getInitialProps(ctx: any) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;

try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App: any) => (props: any) =>
sheet.collectStyles(<App {...props} />),
});

const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
};
} finally {
sheet.seal();
}
}

render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}

🎉 Bravo, our basic setup has been completed.

⚙️ Setting up WebSockets in our app to create a simple chat app:

Let us create a few components for the chat app as shown in the folder tree below before starting our chat app implementation.

├─ src
│ ├─ components
│ │ ├─ Messages
│ │ │ └─ index.tsx
│ │ ├─ Status
│ │ │ └─ index.tsx

🔌 Additional Components:

src/components/Messages/index.tsx :

// src/components/Messages/index.tsx
import React from "react";
import { Avatar, Empty } from "antd";
import { Comment } from "@ant-design/compatible";
import styled from "styled-components";

const Wrapper = styled.div`
height: 60vh;
width: 800px;
padding: 0 30px;
overflow: auto;
`;

const SelfMesssage = styled.div`
display: flex;
justify-content: flex-end;
.self-message {
color: #ffffff;
background-color: #108ee9;
border-radius: 10px;
padding: 10px;
margin-top: 10px;
}
`;

interface iMessage {
messages?: any[];
currentUser?: string;
}

const Messages: React.FC<iMessage> = ({ messages, currentUser }) => {
if (messages && messages?.length >= 1) {
return (
<Wrapper>
{messages.map((message, id) => {
if (message.sender === currentUser) {
return (
<SelfMesssage key={id}>
<div className="self-message">{message.message}</div>
</SelfMesssage>
);
}
return (
<Comment
key={id}
author={<a>{message.sender?.split("@")[0]}</a>}
avatar={
<Avatar
alt={message.sender}
style={{ backgroundColor: "#f56a00" }}
>
{message.sender?.split("")?.[0].toUpperCase()}
</Avatar>
}
content={<p>{message.message}</p>}
/>
);
})}
</Wrapper>
);
}
return (
<Wrapper>
<Empty />
</Wrapper>
);
};

export { Messages };

src/components/status/index.tsx :

// src/components/status/index.tsx
import React from 'react'
import styled from 'styled-components'

interface iStatus {
status?: string
color?: string
}

const StatusComp = styled.span`
display: flex;
flex-direction: row;
align-items: center;
justify-items: left;
.status .status-icon {
width: 20px;
height: 20px;
border-radius: 50%;
border: 1px solid #000;
}

.status .status-icon.disconnected {
background-color: red;
}

.status .status-icon.connected {
background-color: green;
}

.status .status-text {
margin-left: 5px;
}
`

const Status: React.FC<iStatus> = ({ status, color }) => {
return (
<StatusComp className="status" style={{ color: `${color}` }}>
<span className={`status-icon ${status}`}></span>
<span className="status-text">{status}</span>
</StatusComp>
)
}
export { Status }

🚂 Steps to implement in next-js:

Let Us create a index a page that takes the email of the user and the code looks something like this

// pages/index.tsx
import Head from "next/head";
import { Inter } from "next/font/google";
import { useState } from "react";
import { Button, Input, message as antMessage } from "antd";
import styled from "styled-components";

import { useRouter } from "next/router";

const inter = Inter({ subsets: ["latin"] });

export const ChatBox = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 800px;
margin: auto;
.chat-inputs {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex-direction: row;
margin-top: 15px;
gap: 20px;
width: 100%;
}
`;
const LoginWrapper = styled(ChatBox)`
width: 500px;
padding: 30px;
border: 1px solid #1677ff;
margin-top: 70px;
`;

export default function Home() {
const [email, setEmail] = useState("");
const router = useRouter();
return (
<>
<Head>
<title>Simple chat application</title>
<meta
name="description"
content="Simple chat application creating using web socket"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<LoginWrapper>
<Input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
height={40}
/>
<Button
onClick={() => router.push(`/chat/${email}`)}
style={{ marginTop: "10px" }}
color="#3f6600"
disabled={!email}
>
Lets Chat!!!
</Button>
</LoginWrapper>
</main>
</>
);
}

🚶Steps to implement WebSockets in frontend:

  1. We can create a new WebSocket connection in your app by instantiating a new WebSocket object with the URL of your server’s WebSocket endpoint.
    const ws = new WebSocket(‘ws://localhost:8080’);
  2. We can handle events such as onopen, onmessage, and onclose to send and receive data from the server.
    ws.onopen = () => { console.log(‘WebSocket connected’); }; ws.onmessage = (event) => { console.log(‘Received data:’, event.data); };
    ws.onclose = () => { console.log(‘WebSocket disconnected’); };
  3. We can call the send method on our WebSocket object with the data you want to send.
    ws.send(‘Hello, server!’);

Now let us create the chat page under the path of pages/chat/[userId].tsx and implement the above code and the page look like this:

// pages/chat/[userId].tsx
import { Messages } from "@/components/Messages";
import { Status } from "@/components/Status";
import { PageHeader } from "@ant-design/pro-layout";
import { Button, Input, message as antMessage } from "antd";
import Head from "next/head";
import { useState } from "react";
import { SendOutlined, WechatOutlined } from "@ant-design/icons";
import { useRouter } from "next/router";
import { GetServerSidePropsContext } from "next";
import { ChatBox } from "..";

export default function ChatRoomEntry(props: { userId: string }) {
const { userId } = props;

const [ws, setWs] = useState<WebSocket | undefined>(undefined);
const [message, setMessage] = useState("");
const [messages, setMessages] = useState<any[]>([]);
const baseURL = `ws:localhost:8080/websocket?id=${userId}`;

const router = useRouter();

const enterChat = () => {
const ws = new WebSocket(baseURL);
setWs(ws);
ws.onopen = () => {
/**
* i need to call ws.send just because my backend server is config in
* such away that it execpt action:"join-room"
* in your case you might no need to call below send function (in case you are using other backend endpoint)
*/
ws?.send(
JSON.stringify({
action: "join-room",
message: "joining room",
roomId: "123456",
sender: `${userId}`,
})
);
setMessage("");
antMessage.success(`Websocket opened!`);
};

ws.onclose = () => {
antMessage.success(`Websocket closed!`);
setWs(undefined);
};

ws.onmessage = (msg) => {
setMessagesFnc(JSON.parse(msg.data));
};

ws.onerror = (error) => {
antMessage.error(`Websocket error: ${error}`);
};
};

const sendMessage = () => {
if (message && message !== "") {
/**
* json object can be any object
* for my backend endpoint it is expecting below object
*/
ws?.send(
JSON.stringify({
action: "send-message",
message: message,
roomId: "123456", //here roomId is constant/same for all user , you can make it dynamic if you want multiroom chat app
sender: `${userId}`,
})
);
setMessages((prev) => [
...prev,
{
action: "send-message",
message: message,
roomId: "123456",
sender: `${userId}`,
},
]);
setMessage("");
}
};
const handleLogout = () => {
ws?.close();
router.push("/");
};

const setMessagesFnc = (value: any) => {
setMessages((prev) => [...prev, value]);
};
return (
<>
<Head>
<title>Simple chat application</title>
<meta
name="description"
content="Simple chat application creating using web socket"
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<>
<PageHeader
className="site-page-header-responsive"
title="WebChat"
extra={[
<Button key="1" type="primary" onClick={handleLogout}>
Logout
</Button>,
]}
/>
{ws ? (
<ChatBox>
<h1>WebChat</h1>
<Status status="You are online" color="green" />
<Messages messages={messages} currentUser={userId} />
<div className="chat-inputs">
<Input
size="large"
placeholder="Write message"
onChange={(evnt: any) => setMessage(evnt.target.value)}
value={message}
/>
<Button
type="primary"
shape="round"
icon={<SendOutlined />}
onClick={() => sendMessage()}
>
Send Message
</Button>
</div>
</ChatBox>
) : (
<ChatBox style={{ marginTop: "50px" }}>
<h1>WebChat</h1>
<Status status="You are offline" color="red" />
<Button
type="primary"
shape="round"
icon={<WechatOutlined />}
onClick={() => enterChat()}
>
Enter chat
</Button>
</ChatBox>
)}
</>
</main>
</>
);
}
export const getServerSideProps = (ctx: GetServerSidePropsContext) => {
const query = ctx.query;
return {
props: { userId: query.userId },
};
};

🎉 Our chat app is ready!!!

👀Demo:

Let us check our application our entry page UI looks something like this:

index page:

Fig: UI of the index page

After entering the email, we will then go to chat/[userId].tsx page. For the demo, I am using “example@gmail.com”. The UI of this page looks like this:

Fig: UI for WebSocket connection

To establish the connection between WebSocket we should click the “Enter chat” button.

On establishing a WebSocket connection, the UI of our app changes to

Fig: Chat app UI

Now let's create a chat between two users: the first user being example@gmail.comand another user demo1@gmail.com.

Here is the screenshot of chats between user1(example@gmail.com) and user2(demo1@gmail.com)

Fig: UI of User1(example@gmail.com)
Fig: UI of User2(demo1@gmail.com)

👏 Conclusion

This is just a basic tutorial for setting up WebSockets to next-js app, there are alternative ways to connect next-js(front/client) to WebSockets such as using packages like websocket, socket.io-client, stompjs etc. This is just a simple single-room chatting app, we can enhance this for a multi-room chatting app.

Front-end repo:https://github.com/readytowork-org/rnd_front_chat_app_web_socket

Back-end repo:https://github.com/readytowork-org/chat_app

Thank you!!!

Happy coding 🎉

--

--