loading...

A Socket.io tutorial that ISN'T a chat app (with React.js)

captainpandaz profile image Talha Muhammad ・6 min read

A Socket.io tutorial that ISN'T a chat app (with React.js)

Recently, a friend asked for advice for displaying sensor data in real-time (or as near-real-time as possible) on to a dashboard. Specifically, he'd be reading OBD2 data from a drag racing car and wanted to display some data points to an external system. After considering a message bus like Kafka, we decided it was way over kill. I recommended using websockets, specifically, the Socket.io implementation of them. To be fair, I'd never used the Socket.io library, I'd only read about them and I had a surprisingly difficult time finding information on how to use them outside of a chat app or a multiplayer game. To me, translating the chat-room app tutorial found on Socket.io into a different use case was a bit of a process, so I decided to write up this little demo which may be useful to someone in thinking about sockets a different way.

The 3 layers

Here's the super high level design we came up with:
I Like Memes

For our prototype, well be simulating the sensor/reader, setting up a backend server as a pass-through, and implementing a front end consumer.

The Backend

The two packages we'll need are:

const express = require("express");
const http = require("http");
const socketIo = require("socket.io");

//Port from environment variable or default - 4001
const port = process.env.PORT || 4001;

//Setting up express and adding socketIo middleware
const app = express();
const server = http.createServer(app);
const io = socketIo(server);

//Setting up a socket with the namespace "connection" for new sockets
io.on("connection", socket => {
    console.log("New client connected");

    //Here we listen on a new namespace called "incoming data"
    socket.on("incoming data", (data)=>{
        //Here we broadcast it out to all other sockets EXCLUDING the socket which sent us the data
       socket.broadcast.emit("outgoing data", {num: data});
    });

    //A special namespace "disconnect" for when a client disconnects
    socket.on("disconnect", () => console.log("Client disconnected"));
});

server.listen(port, () => console.log(`Listening on port ${port}`));

Lets break this down:

const express = require("express");
const http = require("http");
const socketIo = require("socket.io");

//Port from environment variable or default - 4001
const port = process.env.PORT || 4001;

//Setting up express and adding socketIo middleware
const app = express();
const server = http.createServer(app);
const io = socketIo(server);

If you've used express before, most of this isn't anything new. The only socket.io related stuff we see here is const io = socketIo(server); which sets up a new server instance of socket.io.

//Setting up a socket with the namespace "connection" for new sockets
io.on("connection", socket => {
    console.log("New client connected");

    //Here we listen on a new namespace called "incoming data"
    socket.on("incoming data", (data)=>{
        //Here we broadcast it out to all other sockets EXCLUDING the socket which sent us the data
       socket.broadcast.emit("outgoing data", {num: data});
    });

    //A special namespace "disconnect" for when a client disconnects
    socket.on("disconnect", () => console.log("Client disconnected"));
});

server.listen(port, () => console.log(`Listening on port ${port}`));

Here we're setting up a socket namespace called connection which is where clients will connect to. Once an initial connection is made, we listen on two new namespaces. incoming data and disconnect. The first one is where our "producer" or sensor/reader will be pushing data too.

In our callback we call socket.broadcast.emit("outgoing data", {num: data});. The broadcast flag is special because it allows us to emit data to every client EXCEPT the one that sent us the data. There's no point in sending data back to the producer so we broadcast on yet another namespace, outgoing data.

You'll notice we serialize our incoming data before pushing to our outgoing data namespace. This will make it cleaner on our front end and will give you an idea about how we can send multiple data points in one emit.

The disconnect namespace is reserved for when a client loses connection. It is a good place to do any cleanup. For example, if your server is keeping track of clients that are connected, its a good spot to change the state of the client to disconnected.

The final line is setting up our express app to start listening.

The simulated sensor

Since this is a simulation, all we need to do is send some random data. For the prototype, this was done in pure node.js but there are many client libraries available for socket.io that would most certainly be better for running on an Arduino or other micro-controllers that would attach to a OBD2 sensor. Don't roast me too hard here, it's just a demo.

For this demo, I'll be demoing a "speed" value.

The only package we used here is socket.io-client.

let socket = require('socket.io-client')('http://127.0.0.1:4001');

//starting speed at 0
let speed = 0;

//Simulating reading data every 100 milliseconds
setInterval(function () {
    //some sudo-randomness to change the values but not to drastically
    let nextMin = (speed-2)>0 ? speed-2 : 2;
    let nextMax = speed+5 < 140 ? speed+5 : Math.random() * (130 - 5 + 1) + 5;
    speed = Math.floor(Math.random() * (nextMax - nextMin + 1) + nextMin);

    //we emit the data. No need to JSON serialization!
    socket.emit('incoming data', speed);
}, 100);

Most of this should be pretty self-explanatory, so this section will be short.
let socket = require('socket.io-client')('http://127.0.0.1:4001'); sets up the package to be used. We start out setting out speed variable to 0.

let socket = require('socket.io-client')('http://127.0.0.1:4001'); returns to us the socket connection to be used. We're telling it where its running and what port its running on.

I used setInterval here to simulate read requests between a micro-controller and a sensor every 100 milliseconds. The math to set the next speed is just a "hack-y" way to increase or decrease the speed slightly every time and not to allow the speed to be over 140 or below 0.

socket.emit('incoming data', speed); is where we emit the data through the socket. We emit the data on the incoming data namespace which we've set up on the backend in the previous section.

That's it! Cool huh?

The Dashboard

I built this in React and it was super easy and quick. I'm not going to get into the React details as its out of scope. I'm going to focus on how to consume the data from a socket. That being said, I used react-d3-speedometer to display a speedometer. I've gotta say I'm really impressed with the way it look! I'm also using the same socket.io-client package that we used on the producer.

Here's the React component:

import React, {Component} from "react";
import socketIOClient from "socket.io-client";
import ReactSpeedometer from "react-d3-speedometer"

class App extends Component {
    constructor() {
        super();
        this.state = {
            response: 0,
            endpoint: "http://127.0.0.1:4001"
        };
    }

    componentDidMount() {
        const {endpoint} = this.state;
        //Very simply connect to the socket
        const socket = socketIOClient(endpoint);
        //Listen for data on the "outgoing data" namespace and supply a callback for what to do when we get one. In this case, we set a state variable
        socket.on("outgoing data", data => this.setState({response: data.num}));
    }

    render() {
        const {response} = this.state;
        return (
            <div style={{textAlign: "center"}}>
                <ReactSpeedometer
                    maxValue={140}
                    value={response}
                    needleColor="black"
                    startColor="orange"
                    segments={10}
                    endColor="red"
                    needleTransition={"easeElastic"}
                    ringWidth={30}
                    textColor={"red"}
                />
            </div>
        )
    }
}

export default App;

state.response will hold the value coming from the backend and state.endpoint is just where the server is located. The magic happens in the lifecycle function componentDidMount(). For those of you unfamiliar with React, this function is called when the component is added to the DOM. Hence, this is where well connect to the socket and listen for data.

const socket = socketIOClient(endpoint); simply connects us to the server and opens up a socket connection.

socket.on("outgoing data", data => this.setState({response: data.num})); looks familiar doesn't it? We begin listing on the outgoing data namespace. We have a callback which then takes the response and sets the state to the new value.

Lets take a look at the render function:

    render() {
        const {response} = this.state;
        return (
            <div style={{textAlign: "center"}}>
                <ReactSpeedometer
                    maxValue={140}
                    value={response}
                    needleColor="black"
                    startColor="orange"
                    segments={10}
                    endColor="red"
                    needleTransition={"easeElastic"}
                    ringWidth={30}
                    textColor={"red"}
                />
            </div>
        )
    }

The ReactSpeedometer component has a bunch of props you can pass to it to customize it. Most of it is self-explanatory but you can read all about it here. I used the needleTransition "easeElastic" because it looks cool but "eastLinear" is probably a better choice for a speedometer. Read about the transition effects here.

The render function extracts the current state value for the speed and pass it into the ReactSpeedometer prop named value. This will update the speedometer.

So how does it look!

https://i.imgur.com/D4qzm7o.gif
(Having issues embedding the gif in this post. Sorry!)

It ended up behaving more like a tachometer but turned out pretty cool!

Discussion

markdown guide
 
 

Hey i have a question, i am making chat app ,so where should i put my socket.on() method to continuously update the state without calling it explicitly.
I am noob in react, please help, thanks in advance

 

Hey! Sorry about the late response.

I'm going to assume that you're asking about where to put socket.on() on the client app (which im also assuming is in react).

I'm also going to assume you aren't using hooks since this tutorial did not have hooks (hooks are awesome btw)

If both of these are valid, you can add the "listener" to a components componentDidMount lifecycle method, just like I did above.

You can read more about the method here: reactjs.org/docs/react-component.h...

 

Hey Talha! I've found your post by looking for tuts on best practices for react+web sockets. Thanks its a nice read! Im looking for ways how to optimize my code since its a bit slow right now, I'm using gatsbyJs on the front end and an express on server side to store some mouse events by visitors of the website, I was wondering if you could help me out by looking if im doing something that slows down the whole process.. the app is deployed here jurg.io/ and here's the code for client: github.com/Nejurgis/portfolio and here's the simple server - github.com/Nejurgis/portfolio-server . Forever grateful!

 

Thank you! this is exactly what I was looking for

 
 

i read isnt a chat app and YES!!!

 
 

This is awesome, way more useful than the official chat app tutorial. I feel like I'm ready to go and start using socket.io although will be doing everything with react hooks 👌🏼

 

Good luck! I recently went through a great hooks course and I can't get enough of them! Hooks are great and I definitely recommend using them in conjunction with socket.io

 
 
 

I didn't set up a repo but all code is above