Jay Gould

Using the WebSockets API with React Native and NodeJS

February 25, 2021

WebSockets are a great way to add another level of interactivity to a website or app, providing a way for the client and server to exchange data over a constantly connected protocol. I’d only ever used WebSockets with the use of Socket IO until recently, such as in one of my previous posts about making a loading bar to show a server side process with NodeJS. Socket IO is great because it includes other means of providing a connection between client and server if the client is an older browser without WebSocket support, but with React Native we have confidence that the native WebSockets API can be used without a problem.

Although there’s not loads of helper functionality when using WebSockets API compared to libraries like Socket IO, WebSockets are easy to use and great for peppering in advanced features to a React Native mobile app.

Setting up WebSockets in React Native

The setup is really easy as there’s no packages to install over NPM/Yarn. The WebSockets API is native so is already available for us to use.

import React, { useState, useEffect } from "react"
import { SafeAreaView } from "react-native"

const Home = (props) => {
  // Initiate socket on screen load
  useEffect(() => {
    initiateSocketConnection()
  }, [])

  const initiateSocketConnection = () => {
    // Add URL to the server which will contain the server side setup
    const ws = new WebSocket(`wss://my-server-url.com`)

    // When a connection is made to the server, send the user ID so we can track which
    // socket belongs to which user
    ws.onopen = () => {
      ws.send(
        JSON.stringify({
          userId: 20,
        })
      )
    }

    // Ran when teh app receives a message from the server
    ws.onmessage = (e) => {
      const message = JSON.parse(e.data)
    }
  }

  return (
    <>
      <SafeAreaView></SafeAreaView>
    </>
  )
}

export default Home

Please note: sending the user ID might not be ideal if you’re not using a secure WebSocket connection over WSS. An alternative might be to store a user token or random value to associate a user with a socket connection, storing the random value in a database. I’m simplifying the examples here to show the basics of WebSockets.

This simplified example shows the most basic implementation, with the server URL added as an argument to the WebSocket class, and the user ID sending when the connection is open.

The initiateSocketConnection function is executed when the screen loads with the use of useEffect and an empty array as it’s second argument. This means, depending on your navigation setup with React Native, a new socket connection could be inadvertently set up each time the screen appears to the user. Having multiple socket connections is not ideal as it can get confusing when sending messages between app and server. There are two ways to combat this:

  • Have the server monitor the current sockets and delete any which are no longer needed - this is where the user ID comes in useful so we can close any connections by user once a new connection is made.
  • Have the app close a connection when navigating away from the screen, using ws.close().
  • Again, depending on your navigation setup, have the above socket connection code execute when the app loads in a place in the code which will not fire more than once. This could be in the index file, or wherever makes sense in your app.

I personally like to create a new connection each time like shown in the code above. By creating a new connection when each screen loads, it means the connection is “fresh” and likely not disconnected by the server. For example, if a connection is made when the app loads, and that connection is lost from the server (if the server restarts or there’s a bug), it can be difficult to re-connect the app to the server. By creating a new socket on each app screen, this helps cut down that issue.

Connecting React Native to NodeJS via WebSockets

The setup is a little more complicated on the server side as it needs to be integrated into whatever server side setup you have with Node. My example is for a Node Express server:

// app.js
const WebSocket = require("ws")

import { app } from "./app"

const server = app.listen(app.get("port"))

const wss = new WebSocket.Server({ server })

wss.on("connection", (ws) => {
  // Executed when server receives message from the app
  ws.on("message", async (message) => {
    const received = JSON.parse(message)

    if (received && received.userId) {
      verifyUserAndAddUserIdToSocket(ws, received.userId)
      removeOtherUserLocationSockets(wss, received.userId)
    }
  })
})

// Sets "wss" so Express routes can access the socket instance later - important!
app.set("wss", wss)

There’s no need to install a ws package if you’re on a modern version of Node, and the above shows how the server instance can be passed as an argument to the WebSocket.Server function.

Similar to the front end setup, a callback is fired on message, which is where the Node server receives the user ID sent up from the previous snippet. As sockets can’t transmit JSON, the payload is stringified in the app and parsed on the server to give us access to the user ID here.

At this point you could verify the user ID exists (maybe send up a JWT instead and verify that), and remove any other sockets relating to the user if there are multiple sockets already active.

It’s worth noting the difference between wss and ws in this example. The wss is the WebSocket server - an instance relating to the WebSocket setup as a whole, providing access to things like a list of connected sockets. The ws is a singular socket connection, giving the ability to send a message to a specific client or perform actions on one socket.

Sending a message to a specific client is useful in situations like games or interactive apps where one user is doing something with another user.

Tracking which sockets belong to each user

A useful feature of the WebSockets API is that it gives you the ability to set a socket ID, and retrieve a list of active sockets later on. To set the user ID we need this socket instance (ws) and the user ID:

verifyUserAndAddUserIdToSocket(ws, userId) {
  // Verify user exists, or if a JWT is sent instead of user ID, verify that JWT here...

  ws.id = JSON.stringify({ userId })
}

You can set this ID to be anything. It can also be useful to add other information relating to where the socket originated from, or maybe add a random identifier such as a UUID, as well as, or instead of, a user ID.

We now have a socket which is related to a specific user. Later on when something happens in an Express route, we can send a message back to this specific user:

router.post("/some-action", async (req, res) => {
  // Get the socket server from what we set earlier in app.js
  const wss = req.app.get("wss")

  const databaseResponse = await someFunction()
  const userId = databaseResponse.userId

  let theUsersSocket = []

  wss.clients.forEach((client) => {
    const parsedClientId = JSON.parse(client.id)
    if (parsedClientId === userId) {
      theUsersSocket = client
    }
  })

  sendToThisUsersSocket(theUsersSocket)
})

Initiating socket actions from a route is a real-world example of where sockets really shine, as it’s naturally where you’d want to alert or update a user based on another user’s action.

The wss value is used which was set earlier in the app.js file. Using the wss, we can easily access a list of connected clients, and using the ID we set earlier (containing a user ID per socket connection), we can get the exact socket connection we want, and send whatever message we want.

Thanks for reading!


Senior Engineer at Haven

© Jay Gould 2023, Built with love and tequila.