DEV Community

Cover image for Using WebSockets in React
Nero Adaware
Nero Adaware

Posted on

Using WebSockets in React

Recently I had to consume a WebSocket API in a React application I was working on, So in this article, I will briefly explain how to use it in a React Application.

What is Websocket

According to MDN, The WebSocket API is an advanced technology that makes it possible to open a two-way interactive communication session between the user's browser and a server. With this API, you can send messages to a server and receive event-driven responses without having to poll the server for a reply, Simply WebSocket helps you maintain two-way communication between the client(in my case React app) and the Server.

Why did I need WebSocket

I worked on a project that required me sending a ping to the server every 30 seconds to tell the server that the application was still online and also keep track of a user that is logged on to the application and the duration they have been online. I don't want to go into too many details about that but I needed to constantly "communicate" with the server and using REST API's for that would have been inefficient.

Basic Setup for React

Usually, I try to use only one instance of WebSocket high up my component tree then pass it down to other components that need to use the WebSocket instance to either listen or send messages to the server; this is assuming that you listening to a particular WebSocket instance in your child components.

class Main extends Component {
    ......

    // instance of websocket connection as a class property
    ws = new WebSocket('ws://localhost:3000/ws')

    componentDidMount() {
        this.ws.onopen = () => {
        // on connecting, do nothing but log it to the console
        console.log('connected')
        }

        this.ws.onmessage = evt => {
        // listen to data sent from the websocket server
        const message = JSON.parse(evt.data)
        this.setState({dataFromServer: message})
        console.log(message)
        }

        this.ws.onclose = () => {
        console.log('disconnected')
        // automatically try to reconnect on connection loss

        }

    }

    render(){
        <ChildComponent websocket={this.ws} />
    }
}

Enter fullscreen mode Exit fullscreen mode

In the snippet above I called it Main component because I assumed it should be like a parent to the child components that need to use the WebSocket instance. First of all, we create a new instance of the WebSocket as a class property ws. Then in the componentDidMount method we can subscribe and listen to some events provided to us by WebSocket.

  • onopen : The onopen event listener is called when the WebSocket connection is established.
  • onmessage: The onmessage event is sent when data is received from the server.
  • onclose : The onclose listener is called when the WebSocket connection is closed.

So all these are set up in the componentDidMount because we want these event listeners available when the component is rendered in the DOM. Also, we can pass the instance of the WebSocket as props to the child component as we did in the <ChildComponent/> so we could listen to any event on that WebSocket instance without having to create a new instance in every component we need to use that WebSocket in.

But there is a problem with this setup, once there is an error or the WebSocket connection closes for some reason i.e server is down or network issues e.t.c The connection won't be reestablished until you the componentDidMount is called again maybe through a refresh of that page. And I don't think this is what we want.

Advanced Setup

This setup was adapted from two StackOverflow answers, How to reconnect to WebSocket after close connection and WebSocket: How to automatically reconnect after it dies.

class Main extends Component {
    constructor(props) {
        super(props);

        this.state = {
            ws: null
        };
    }

    // single websocket instance for the own application and constantly trying to reconnect.

    componentDidMount() {
        this.connect();
    }

    timeout = 250; // Initial timeout duration as a class variable

    /**
     * @function connect
     * This function establishes the connect with the websocket and also ensures constant reconnection if connection closes
     */
    connect = () => {
        var ws = new WebSocket("ws://localhost:3000/ws");
        let that = this; // cache the this
        var connectInterval;

        // websocket onopen event listener
        ws.onopen = () => {
            console.log("connected websocket main component");

            this.setState({ ws: ws });

            that.timeout = 250; // reset timer to 250 on open of websocket connection 
            clearTimeout(connectInterval); // clear Interval on on open of websocket connection
        };

        // websocket onclose event listener
        ws.onclose = e => {
            console.log(
                `Socket is closed. Reconnect will be attempted in ${Math.min(
                    10000 / 1000,
                    (that.timeout + that.timeout) / 1000
                )} second.`,
                e.reason
            );

            that.timeout = that.timeout + that.timeout; //increment retry interval
            connectInterval = setTimeout(this.check, Math.min(10000, that.timeout)); //call check function after timeout
        };

        // websocket onerror event listener
        ws.onerror = err => {
            console.error(
                "Socket encountered error: ",
                err.message,
                "Closing socket"
            );

            ws.close();
        };
    };

    /**
     * utilited by the @function connect to check if the connection is close, if so attempts to reconnect
     */
    check = () => {
        const { ws } = this.state;
        if (!ws || ws.readyState == WebSocket.CLOSED) this.connect(); //check if websocket instance is closed, if so call `connect` function.
    };

    render() {
        return <ChildComponent websocket={this.state.ws} />;
    }
}
Enter fullscreen mode Exit fullscreen mode

The advanced setup above simply ensures the WebSocket is always trying to connect if the server goes down or if there is network failure, so whenever the server is back up the client is reconnected.

I will run through what this setup does, the connect method is called to initiate the WebSocket connection in the componentDidMount. A class property called timeout is declared and set to 250ms then we have two functions connect and check I will go into details about what these functions do.

  • check - This function is used to check if there is no WebSocket instance or the WebSocket connection is closed, if so the connect function is called.

  • connect - This function is basically managing the WebSocket connection, here we listen to the onopen, onclose and onerror events. In the onopen listener, the websocket instance is added to the state so it could be passed as props to child components that want to listen to it. Then the timeout variable is set back 250ms and the setInterval is cleared.

In the onclose listener the timeout value is increased and the check function is called in a setTimeout with the incremented timeout value, once the timeout value becomes greater than 10000ms(10 seconds) it stops incrementing. I did this to prevent aggressive attempts to reconnect to the server, instead it delays for a given period before it tries to reconnect.

There are libraries that help you achieve this like ReconnectingWebSocket but this library and my setup don't implement the Exponential Backoff algorithm which helps manage server flood when a lot of clients are trying to reconnect to the server. A library called @gamestdio/websocket implements the Exponential Backoff, so you could use it if you are dealing with a large number of client applications.

Sending a message with the WebSocket

There is gotcha in this advanced setup being that when the WebSocket connection is closed onclose listener the WebSocket instance is might be set to null for some moments, the reason for this is to ensure we don't open a new WebSocket instance anytime the connection is closed and it opens again. The problem here is that if you try to send data to the server when the websocket instance is null it might break your application, so how do we solve this? The answer is to use try catch block anywhere you need to send data in your components.



class ChildComponent extends Component {

    sendMessage=()=>{
        const {websocket} = this.props // websocket instance passed as props to the child component.

        try {
            websocket.send(data) //send data to the server
        } catch (error) {
            console.log(error) // catch error
        }
    }
    render() {
        return (
            <div>
                ........
            </div>
        );
    }
}

export default ChildComponent;
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope this tutorial helps you setup WebSockets in your react application because that was my motivation to write this post. I am new to using WebSockets on the client-side so if you think there is anything that could be done better make a comment.

Top comments (11)

Collapse
 
lai32290 profile image
Lai Xuancheng

My experience about WebSocket + React is implementing the connection in Redux middleware, including all the received event handler to dispatch Redux action automatically and to it has to do.

And dispatching a action like:
{
type: "SOCKET_XXXXXX"
}

to send event from client to server.

I'll try your solution for next time, it seems really work.

Thanks for share.

Collapse
 
djoleb profile image
Djordje Bajic

Hello Nero!

Thanks for taking your time to write this post.
Can you consider writing one with websockets + subscription? For example you connect to ws://localhost/something and subscribe to /posts/get?

Collapse
 
finallynero profile image
Nero Adaware

Alright, will look into it.
Thanks Djordje.

Collapse
 
guico33 profile image
guico33

What's up with mixing var and let instead of just using const?

Collapse
 
osamaeshtiaq profile image
osamaeshtiaq

What npm package to install for this?

Collapse
 
esa profile image
E-sa

Mine works without any packages.

Collapse
 
umpurevdalai profile image
Purevdalai

You don't need to install any packages. It works without any packages

Collapse
 
ceikit profile image
ceikit

This looks good but unfortunately does not work if you try to pass ws to more than one child component.
I find that the first child ends up not receiving any data.
Would you have a solution for this?

Collapse
 
krishteja18 profile image
krishteja18

You can use react-websocket npm package to easily reconnect

Collapse
 
emekalites profile image
Chukwuemeka Ihedoro • Edited

Nice article bro, helped me a lot. Thanks.

Collapse
 
patrickduc profile image
Patrick Duc

Nice article, thank you.