Doğacan Bilgili A software developer who is also into 3D-modeling and animation.

Implementing WebSocket communication in Next.js

5 min read 1517

Logo Over a Gray-White Background

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:


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.

Socket Updating in Real-Time

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.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Doğacan Bilgili A software developer who is also into 3D-modeling and animation.

22 Replies to “Implementing WebSocket communication in Next.js”

  1. 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?

      1. 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 :/

        1. 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.

    1. 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.

      1. 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.

    1. Did you find the solution? i have the same problem with my app in production

  2. 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

  3. 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….

  4. 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?

  5. 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 🙂

  6. 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()
    }
    “`

    1. 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.

  7. This isn’t working, I keep getting a 404 localhost:3000/socket.io?EIO=4&transport=polling&t=OWzE3BU 404 (N

Leave a Reply