How to Avoid Multiple WebSocket Connections in a React Chat App

Concurrent WebSocket connections in a chat app are a surefire way to kill your app’s performance and increase your bill. In this post, you’ll learn how to spot and avoid concurrent connections using Stream’s React Chat SDK.

Dillion M.
Dillion M.
Published November 30, 2021 Updated December 1, 2021
Avoid Multiple WebSocket Connections in a React Chat App

WebSockets are at the core of every chat app. At Stream, whenever you connect a user to a channel, you create a WebSocket connection. That means for every connected user, there’s at least one connection open.

But, did you know it’s possible for a single user to connect multiple times? This is what we refer to as “concurrent connections”. Whether you’re a lean startup or a Fortune 500 company, those concurrent connections will drive up your bill.

As a developer, you need to avoid these concurrent connections as much as possible to manage costs. In this article, you’ll see how Stream works with WebSockets and how you can avoid multiple WebSockets when your users unwittingly create them.

What is a WebSocket connection?

WebSockets are a type of internet protocol that allows your browser to communicate with a server in real-time.

The difference between WebSockets and HTTP protocols is that WebSockets don't transfer data using the traditional request/response model. With WebSockets, updates are sent when they are available.

A WebSocket creates a bi-directional communication flow where the server does not have to wait for the browser to request a resource before delivering it.

This technique allows the server to send resources to many connected browsers at once, which is beneficial in interactive applications like chat and gaming apps.

Stream uses WebSockets for real-time communication in chat applications. When a user connects to Stream, Stream's server creates a socket connection on the user's device. As stated earlier, when a connection is open, it is a concurrent connection. More users means more concurrent connections, and therefore, more expenses.

The Cost of Concurrent Connections

The more users that are connected, the more resources the server requires to manage them. And this calls for more money on platforms that offer such services, just like Stream.

If you have 20 different users who are concurrently connected, there’s nothing you can do to avoid it. But what if one user opens multiple connections?

Even though it's possible, you would want to avoid this situation because one connection should be enough.

With this being said, there are two ways one user can create multiple connections in your chat app:

  • Opening multiple connections in the same browser tab
  • Opening multiple connections in different tabs (one connection per tab)

Multiple Connections in the Same Tab

Multiple connections in one browser tab is usually the result of a development bug. The connectUser method of the chat client instance connects a user to Stream. It creates a WebSocket connection with Stream, and it sends and receives events and payloads from the server.

In previous versions of Stream's React Chat SDK, you create a Stream instance using the new constructor like this:

javascript
1
2
const client = new StreamChat('API_KEY'); client.connectUser(...)

The disadvantage of using this method is that you can create multiple instances of the chat client, resulting in numerous WebSocket connections when connecting the user.

A practical example is an infinite useEffect call as seen below:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
const [chatClient, setChatClient] = useState(null); const userObj = {}; const userToken = ''; useEffect(() => { const initChat = async () => { const client = new StreamChat(apiKey); await client.connectUser(userObj, userToken); setChatClient(client); }; initChat(); }, [chatClient]);

The constant useEffect updates at each render can slow down the UI. Here's the DevTools showing the WebSocket connections:

Multiple "pending" websocket connections

You can see that there are many “pending” connections indicating that they are open.

Let's say there was a 2-second delay between the connectUser calls. We can imitate this using the following code:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
const [chatClient, setChatClient] = useState(null); useEffect(() => { const initChat = async () => { const client = new StreamChat(apiKey); await client.connectUser(userObj, userToken); setChatClient(client); }; setTimeout(() => { initChat(); }, 2000); }, [chatClient]);
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
Multiple open connections with fast UI

With this, the UI is better, which improves the user experience, but there are still multiple unnecessary WebSocket connections.

To avoid these connections, you can use the new way of creating a Stream instance — StreamChat.getInstance('API_KEY'). This way, you can create a single instance of the chat client. Only one WebSocket connection will be opened, even in a continuous call of the useEffect hook.

However, you'd still want to avoid continuous useEffect calls as this may affect the performance of your application.

Multiple Connections in Different Tabs

Now, this may not be a development bug, but developers have to anticipate this possibility. As a user, you can open multiple tabs for the same chat application.

You would get the same events, UI updates, and payloads in each tab, but you’re only using one tab at a time. So, what's the point of leaving the other connections open?

To solve this, we can check if the browser tab of the chat app is active. When it's not, we disconnect the user, and when it becomes active again, we reconnect the user. This way, if the user is using different tabs for the chat app, they would only have one connection at a time.

At the same time, if the user is doing something else on a different tab or using a separate application, your Stream resources would be managed.

The Page Visibility API (which is currently supported by all browsers) provided by your browser can tell you when a page is active or not.

Now, let's see how we can disconnect a user when a tab is inactive. Let's say you had the following code to connect a user:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const App = () => { const [chatClient, setChatClient] = useState(null); useEffect(() => { const initChat = async () => { const client = new StreamChat(apiKey); await client.connectUser(userObj, userToken); setChatClient(client); }; initChat(); return () => chatClient?.disconnectUser(); }, []); if (!chatClient) return null; return () };

The code snippet above is an excerpt from Stream’s Social Messenger React example.

Now let's use the visibilitychange event to disconnect the user when the tab is inactive. Here's the updated code:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const App = () => { const [chatClient, setChatClient] = useState(null); const [connected, setConnected] = useState(false); useEffect(() => { const initChat = async () => { const client = new StreamChat(apiKey); await client.connectUser(userObj, userToken); setChatClient(client); setConnected(true); }; initChat(); }, []); useEffect(() => { if(!chatClient) return; const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { console.log('left tab'); chatClient.disconnectUser(); setConnected(false); } else { console.log('returned to tab'); chatClient.connectUser(userObj, userToken); setConnected(true); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [chatClient]); if (!chatClient || !connected) return null; return () };
Disconnect a user changing tabs

With the code above, when users navigate to a different tab, they are disconnected from the previous tab until it is active again.

We need the extra connected state because a disconnected user cannot use a channel, as seen in the error below:

error disconnecting

But what happens if a user leaves a tab only to return seconds later? You’d have to disconnect and reconnect them, which could result in a bad user experience.

To improve the experience, we can add a delay before disconnecting the user. Here's how:

javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
useEffect(() => { if (!chatClient) return; let timeoutId; // highlight-line const handleVisibilityChange = () => { if (document.visibilityState === 'hidden') { timeoutId = setTimeout(() => { chatClient.disconnectUser(); setConnected(false); }, 5000); } else { clearTimeout(timeoutId); if (!connected) { chatClient.connectUser(userObj, userToken); setConnected(true); } } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { document.removeEventListener('visibilitychange', handleVisibilityChange); }; }, [chatClient]);

With the code above, when the user navigates to a different tab, the previous tab has a visibilityState of hidden, and a timeout set to five seconds. After five seconds, the WebSocket connection is closed, and we update the connected state to false.

We also keep track of the timeoutId such that when the user navigates back to the previous tab, we stop the timeout from executing (if it hasn't already). But if the user has been disconnected already, we reconnect them.

Conclusion

In this article, you learned about concurrent connections, the cost of having them in your app, and how to save money by avoiding multiple connections made by a single user.

You can also check out this little guide on debugging multiple WebSocket connections.

Happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->