WebSockets with functional components

Viewed 12547

I'm using websockets in my React application. The application is supposed to consist of function components only.

Imagine the application consists of two "tabs": one irrelevant, and the other one, a chat using websockets. Given that, we want to whip up a websocket connection once the user enters the chat tab.

How should the component handle the websocket object? Since we want to clean up once the user switches back to the other tab (WebSocket.close()), it sounds like we should be using an effect hook here.

const Chat = () => {
    const [messages, setMessages] = useState([]);
    useEffect(() => {
        const webSocket = new WebSocket("ws://url");
        webSocket.onmessage = (message) => {
            setMessages(prev => [...prev, message.data]);
        };
        return () => webSocket.close();
    }, []);
    return <p>{messages.join(" ")}</p>;
};

Works! But now, imagine we want to reference the webSocket variable somewhere outside the useEffect scope - say, we want to send something to the server once the user clicks a button.

Now, how should it be implemented? My current idea (albeit a flawed one, I feel) is:

const Chat = () => {
    const [messages, setMessages] = useState([]);
    const [webSocket] = useState(new WebSocket("ws://url"));
    useEffect(() => {
        webSocket.onmessage = (message) => {
            setMessages(prev => [...prev, message.data]);
        };
        return () => webSocket.close();
    }, []);
    return <p>{messages.join(" ")}</p>;
};

Kinda works, nevertheless I feel like there's a better solution.

2 Answers

As @skyboyer has written in a comment under your question, you could use useRef hook to hold WebSocket and it will be more correct since you are not going to change or recreate WebSocket object. So you don't need useState hook.

The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class. more

So you could change your code to:

const Chat = () => {
    const [messages, setMessages] = useState([]);
    const webSocket = useRef(null);

    useEffect(() => {
        webSocket.current = new WebSocket("ws://url");
        webSocket.current.onmessage = (message) => {
            setMessages(prev => [...prev, message.data]);
        };
        return () => webSocket.current.close();
    }, []);
    return <p>{messages.join(" ")}</p>;
};

probably a good option is to have in a separate WS instance for manage the WS operations and used it where you need it, for example.

class WebSocketClient {
    static instance = null;
    callbacks = {};

    static getInstance() {
        if (!WebSocketClient.instance) WebSocketClient.instance = new WebSocketClient();
        return WebSocketClient.instance;
    }

    constructor() {
        this.socketRef = null;
    }

    addCallbacks = (...callbacks) => this.callbacks = { ...callbacks };

    connect = () => {
        const path = 'YOUR_SOCKET_PATH';
        this.socketRef = new WebSocket(path);
        this.socketRef.onopen = () => {
            console.log('WebSocket open');
        };

        this.socketRef.onmessage = e => {
            this.socketNewMessage(e.data);
        };

        this.socketRef.onerror = e => {
            console.log(e.message);
        };

        this.socketRef.onclose = () => {
            console.log("WebSocket closed let's reopen");
            this.connect();
        };
    }

    state = () => this.socketRef.readyState;

    waitForSocketConnection = (callback) => {
        const socket = this.socketRef;
        const recursion = this.waitForSocketConnection;
        setTimeout(
            () => {
                if (socket.readyState === 1) {
                    console.log("Connection is made")
                    if (callback != null) {
                        callback();
                    }
                    return;
                } else {
                    console.log("wait for connection...")
                    recursion(callback);
                }
            },
        1);
    }

}

export default WebSocketClient.getInstance();

So in your component will be.

import React, { useEffect } from 'react';
import WSC from 'wsc';


const Test = ({}) => {

    useEffect(()=>{
        WSC.connect();
        WSC.waitForSocketConnection(()=>{
            'HERE_YOUR_CALLBACKS'
        });
    },[])
}

With this way always return the same instance.

Related