Real-time communication on the web uses the WebSocket API, which is a full-duplex communication channel over a single TCP connection. This makes it possible to have multiple clients connecting to a server and sharing real-time data, which enables various experiences, such as multiplayer games, interactive documents, or chat rooms.
What is Socket.io?
The most famous WebSocket wrapper for Node.js is Socket.io. It’s a package with a Node.js server and a client library that’s used on the browser. In this article, I will use Socket.io to show a WebSocket connection between a server and a Next.js application.
It’s important to note that serverless functions on Vercel do not support WebSockets. Although Vercel is not the only option for deploying a Next.js application, this limitation might be a deal-breaker for some.
Vercel recommends using third-party solutions for real-time communication on deployments to their platform. They also provide a guideline for integrating one of these third-party solutions, which is called Pusher.
We’ll start with creating a Next.js application by using the Create Next App package.
Run npx create-next-app websocket-example
, where websocket-example
is the name of the project. This generates a boilerplate app.
Creating the server side
When you navigate into the project folder, you will see the pages/api
folder. This is where we create the endpoints that the client will consume. Any file under the pages/api
is treated as an endpoint that is mapped to /api/*
.
Create a socket.js
file under pages/api
. This file will handle the WebSocket connection by ensuring that there is only one instance of the WebSocket, which we will access by making a request to /api/socket
on the client.
By default, Next.js has its own server and you don’t need any configuration unless a custom server is required. For this basic WebSocket example, we don’t need to configure a custom server.
In the socket.js
file, we need to check if there is already a socket connection opened in the HTTP server. If not, we plug Socket.io.
As you might already know, for an API route to work, Next.js requires a function to be exported as default, which has the req
and res
parameters corresponding to http.IncomingMessage
and http.ServerResponse
, respectively.
Here’s an example of an API route.
export default function handler(req, res) {}
Now we need to use this pattern and initialize a socket connection where there are not any already. In order to plug the HTTP server to Socket.io, we use the Server
class from the Socket.IO
package.
Inside the default exported function, we do a check on res.socket.server.io
, which does not exist by default and shows that the socket connection is not in place. If this io
object on res.socket.server
is non-existent, then we start a new socket connection by instantiating a Server
, which takes the res.socket.server
as the input.
Next, we set the returned object to res.socket.server.io
so that when there is a new request after the instantiation, res.socket.server.io
will not be undefined
.
Outside the if
statement, we simply end the request to prevent it from resulting in a stalled request.
import { Server } from 'Socket.IO' const SocketHandler = (req, res) => { if (res.socket.server.io) { console.log('Socket is already running') } else { console.log('Socket is initializing') const io = new Server(res.socket.server) res.socket.server.io = io } res.end() } export default SocketHandler
Using the endpoint on the client side
So far we have created an endpoint to initialize the WebSocket connection. Now we need to use this endpoint on the client side.
Under the /pages
directory, you will find the index.js
file, which is the route for the main directory. Inside this file, we are going to implement a component with an input field. We’ll then emit the changes in the input field to the server to be broadcasted back to all other connected clients. This is a basic example to show real-time communication.
The first thing we need to do in the component is establish the socket connection. To do so, we are going to use the useEffect
Hook with an empty array dependency, which is basically the equivalent of the componentDidMount
lifecycle Hook.
SocketInitializer
is an async function because we need to make a call to the /api/socket
endpoint to open the socket connection.
Once this call is resolved, we then initialize the io()
object and set this to a variable outside the function scope so that it stays the same throughout the lifecycle of the component.
Then, we use socket.on()
to listen for a connect
emit and log a message to ensure the client is connected. This is a reserved event and should not be emitted manually. You can find the list of reserved events here.
Now, we need to call this function inside the useEffect
Hook with a []
dependency. The reason we wrap the socketInitializer
inside another function is because effect callbacks should be synchronous to prevent race conditions.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
import { useEffect } from 'react' import io from 'Socket.IO-client' let socket const Home = () => { useEffect(() => socketInitializer(), []) const socketInitializer = async () => { await fetch('/api/socket') socket = io() socket.on('connect', () => { console.log('connected') }) } return null } export default Home;
If you now visit the port on your local host, which runs the development server, you will see the “Connected” message logged in the console.
If you check the terminal screen you started the development server from, you will also see “Socket is initializing” logged after the first client connected.
If you connect another client by visiting the development server on another browser tab or screen, you will see the “Socket is already running” message printed out.
Emitting messages with Socket.io
Now that we have the socket connection in place, we can emit messages between clients and the server.
First, we are going to create a controlled input field. All we need is a state and a handler function for the onChange
event of the input field.
const [input, setInput] = useState('') const onChangeHandler = (e) => { setInput(e.target.value) socket.emit('input-change', e.target.value) } return ( <input placeholder="Type something" value={input} onChange={onChangeHandler} /> )
Inside onChangeHandler
, we set the input
state to a new value and then emit this change to the server as an event. The first parameter to socket.emit()
is the unique event name, which is input-change
, and the second parameter is the message. In our case, this is the value of the input field.
Next, we need to handle this event in the server by listening for the specific input-change
event.
By registering a connection
listener, we make sure that the socket connection is made. Afterward, inside the callback function, we subscribe for the input-change
event through the socket.on()
function, which takes an event name and a callback function as parameters.
io.on('connection', socket => { socket.on('input-change', msg => { socket.broadcast.emit('update-input', msg) }) })
Then, inside the callback function, we use socket.broadcast.emit()
, which emits a message to all clients except the one that emitted the input-change
event. So, only other clients receive the msg
, which is the text input value sent by one of the clients.
The final version of the socket.js
file is as follows.
>import { Server } from 'Socket.IO' const SocketHandler = (req, res) => { if (res.socket.server.io) { console.log('Socket is already running') } else { console.log('Socket is initializing') const io = new Server(res.socket.server) res.socket.server.io = io io.on('connection', socket => { socket.on('input-change', msg => { socket.broadcast.emit('update-input', msg) }) }) } res.end() } export default SocketHandler
Now, back to the client. This time, we need to handle the broadcasted event, which was update-input
. All we need to do is subscribe to this event with socket.on()
and call the setInput
function inside the callback to update the input value with the value sent from the server.
import { useEffect, useState } from 'react' import io from 'Socket.IO-client' let socket; const Home = () => { const [input, setInput] = useState('') useEffect(() => socketInitializer(), []) const socketInitializer = async () => { await fetch('/api/socket'); socket = io() socket.on('connect', () => { console.log('connected') }) socket.on('update-input', msg => { setInput(msg) }) } const onChangeHandler = (e) => { setInput(e.target.value) socket.emit('input-change', e.target.value) } return ( <input placeholder="Type something" value={input} onChange={onChangeHandler} /> ) } export default Home;
Below is a demonstration of two clients connecting to the socket and updating in real-time in accordance with the data sent to the server.
Conclusion
In this article, we learned how to integrate Socket.io into Next.js in order to start a WebSocket connection to share information back and forth between the client and server in real-time.
We also created an endpoint in the Next.js backend, which we then made a request to in the client to initialize the socket connection. After that, the rest of the communication between the server and the client has been handled through the socket connection.
Great article! Works great on localhost. I seem to be stuck at deployment though. I assume that since serverless solutions such as Vercel cannot run this then either can netlify or amplify etc? Right? What would be a good hosting solution for a nextjs app that uses socket.io like how you have outlined here?
Update. Got it working on Heroku.
I had a website on vercel. I was trying to do that. As you said it works locally but it was not working on prod. :/ It seems heroku is different but I do not want to move my project there. I wish Vercel provide us the same thing :/
That’s because Vercel runs serverless functions; they’re not meant to run 24/7 nor are they a single instance (they scale up and down). You need a custom server if you want to do WebSockets, and Vercel doesn’t support hosting custom servers.
Next.js is good for handling requests and sending responses, if you want to do WebSockets, use a 3rd party service like Pusher or host your own solution beside Next.js.
Socket.io is not WebSocket, you should change your title.
Thanks for your comment. Since Socket.io is built on top of the WebSocket protocol, we don’t feel the title is inaccurate or misleading.
It is misleading because when searching for information about the ‘ws’ package (aka WebSocket) one gets this article, which talks about Socket.IO. The API offered by the 2 packages are different so this article is misleading.
It is working on local but not working on vercel server https://socket-chat-app-next-js.vercel.app/
Did you find the solution? i have the same problem with my app in production
apparently it doesn’t like the wrapping of the socketInitializer function. I get the following:
Warning: useEffect must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => …) or returned a Promise. Instead, write the async function inside your effect and call it immediately:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// …
}
fetchData();
}, [someId]); // Or [] if effect doesn’t need props or state
also, the Google and Twitter login mechanisms on this site are broken
basically, this fixes it: `useEffect(() => {socketInitializer()}, [])`
If you want to use socket with typescript
https://codesandbox.io/s/piffv?file=/src/pages/api/socketio.ts:164-173
You save my day, thanks!!
this is not working in my localhost, first i’ve write the codes that not worked, then i’ve copy all codes and paste , that not works too….
“TypeError: destroy is not a function”
any solution?
useEffect(() => {
socketInitializer()
})
Does this method work external things, like accessing it from react native or ionic.
Because I am not able to access my nextjs socket. Is there any tricks?
Thank you for the article
I’m struggling to see *why* this is working though (even though I can see that it does).
According to Next-JS, the res object is a `http.ServerResponse` object. This does indeed contain a `socket` which is a `net.Socket object` (https://nodejs.org/api/net.html#class-netsocket).
The documentation never mentions a `server` property on `net.Socket` though. where is that coming from? Why isn’t it documented in Node, how did you figure out it exists, why is Typescript also denying it exists? etc. etc.
I’m happy it works, but it feels weird to use something that (as far as I can see) is an undocumented hidden feature 🙂
Could you explain the lifecycle of the API route? From my understanding each client will have a separate response object, thus a separate SocketIO instance? For how long will it stay active?
Seems to me if I wish to do client-client communication I will need a customer server.
“`
const SocketHandler = (req, res) => {
if (res.socket.server.io) {
console.log(‘Socket is already running’)
} else {
console.log(‘Socket is initializing’)
const io = new Server(res.socket.server)
res.socket.server.io = io
}
res.end()
}
“`
You are correct. I left a comment above where I explain that yes, you need a custom server to handle this. Also, WebSockets won’t let you set up P2P/client-client communication, you can look into WebRTC for that.
This isn’t working, I keep getting a 404 localhost:3000/socket.io?EIO=4&transport=polling&t=OWzE3BU 404 (N
Same here, this is crazy, I tried everything!