Reconnecting web socket using React Hooks - reactjs

I want to establish a websocket connection with the server. Reconnect after 5 seconds if the connection closes. I am using React Hooks and so far achieved this
import React, { useRef, useState, useEffect } from 'react';
function App() {
const wsClient = useRef(null);
const [wsState, setWsState] = useState(true)
useEffect(() => {
    wsClient.current = new WebSocket(url);
    console.log("Trying to open ws");
    setWsState(true)
    
    wsClient.current.onopen = () => {
      console.log('ws opened');
      wsClient.current.send('{"type" : "hello"}')
    };
    wsClient.current.onclose = (event) => {
// Parse event code and log
      setTimeout(() => {setWsState(false)}, 5000)
      console.log('ws closed');
}
wsClient.current.onmessage = ((event) => {
      // DO YOUR JOB
})
return () => {
      console.log('ws closed');
      wsClient.current.close();
    }
  }, [wsState]);
return (
      <div className="App">
        <Header />
        <MainBody />
      </div>
  );
}
This is creating exponentially increasing number of retries when it is unable to connect with server, if I remove setTimeout and use simple setState it is working normally.
I am unable to understand the issue and also suggest what is the best practice to achieve my goal.

I'm not convinced that an effect is the best place for this. If it's application-level, it may be simpler to implement it in its own module, and bring that in, where needed.
Nevertheless, to get this to work, you should consider that you're managing two separate lifecycles: the component lifecycle, and the websocket lifecycle. To make it work as you want, you have to ensure that each state change in one aligns with a state change in the other.
First, keep in mind that your effect runs every time the dependencies in the array change. So, in your example, your effect runs every time you set wsState.
The other thing to keep in mind is that your cleanup function is called every time wsState changes, which you're doing twice in your effect (setting it to true on open, and false on close). This means that when you create a new socket, and it fails to connect, the close event fires, and it queues up a state change.
Each time it attempts to connect, it sets wsState to true (which queues a re-run of your effect), tries and fails to connect, finally setting another timeout, which updates the state to false. But, not before the effect runs again, trying to set the state to true, etc.
To fix this, start with the effect lifecycle. When should your effect run? When should it be cleaned up? A few thoughts:
The effect should run once during the first render, but not during subsequent renders
The effect should be cleaned up when the WebSocket disconnects
The effect should be re-run after a timeout, triggering a reconnect
What does this mean for the component? You don't want to include the WS state as a dependency. But, you do need state to trigger it to re-run after the timeout.
Here's what this looks like:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
export default function App() {
const clientRef = useRef(null);
const [waitingToReconnect, setWaitingToReconnect] = useState(null);
const [messages, setMessages] = useState([]);
const [isOpen, setIsOpen] = useState(false);
function addMessage(message) {
setMessages([...messages, message]);
}
useEffect(() => {
if (waitingToReconnect) {
return;
}
// Only set up the websocket once
if (!clientRef.current) {
const client = new WebSocket(URL);
clientRef.current = client;
window.client = client;
client.onerror = (e) => console.error(e);
client.onopen = () => {
setIsOpen(true);
console.log('ws opened');
client.send('ping');
};
client.onclose = () => {
if (clientRef.current) {
// Connection failed
console.log('ws closed by server');
} else {
// Cleanup initiated from app side, can return here, to not attempt a reconnect
console.log('ws closed by app component unmount');
return;
}
if (waitingToReconnect) {
return;
};
// Parse event code and log
setIsOpen(false);
console.log('ws closed');
// Setting this will trigger a re-run of the effect,
// cleaning up the current websocket, but not setting
// up a new one right away
setWaitingToReconnect(true);
// This will trigger another re-run, and because it is false,
// the socket will be set up again
setTimeout(() => setWaitingToReconnect(null), 5000);
};
client.onmessage = message => {
console.log('received message', message);
addMessage(`received '${message.data}'`);
};
return () => {
console.log('Cleanup');
// Dereference, so it will set up next time
clientRef.current = null;
client.close();
}
}
}, [waitingToReconnect]);
return (
<div>
<h1>Websocket {isOpen ? 'Connected' : 'Disconnected'}</h1>
{waitingToReconnect && <p>Reconnecting momentarily...</p>}
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}
In this example, the connection state is tracked, but not in the useEffect dependencies. waitingForReconnect is, though. And it's set when the connection is closed, and unset a time later, to trigger a reconnection attempt.
The cleanup triggers a close, as well, so we need to differentiate in the onClose, which we do by seeing if the client has been dereferenced.
As you can see, this approach is rather complex, and it ties the WS lifecycle to the component lifecycle (which is technically ok, if you are doing it at the app level).
However, one major caveat is that it's really easy to run into issues with stale closures. For example, the addMessage has access to the local variable messages, but since addMessage is not passed in as a dependency, you can't call it twice per run of the effect, or it will overwrite the last message. (It's not overwriting, per se; it's actually just overwriting the state with the old, "stale" value of messages, concatenated with the new one. Call it ten times and you'll only see the last value.)
So, you could add addMessage to the dependencies, but then you'd be disconnecting and reconnecting the websocket every render. You could get rid of addMessages, and just move that logic into the effect, but then it would re-run every time you update the messages array (less frequently than on every render, but still too often).
So, coming full circle, I'd recommend setting up your client outside of the app lifecycle. You can use custom hooks to handle incoming messages, or just handle them directly in effects.
Here's an example of that:
import React, { useRef, useState, useEffect } from 'react';
const URL = 'ws://localhost:8888';
function reconnectingSocket(url) {
let client;
let isConnected = false;
let reconnectOnClose = true;
let messageListeners = [];
let stateChangeListeners = [];
function on(fn) {
messageListeners.push(fn);
}
function off(fn) {
messageListeners = messageListeners.filter(l => l !== fn);
}
function onStateChange(fn) {
stateChangeListeners.push(fn);
return () => {
stateChangeListeners = stateChangeListeners.filter(l => l !== fn);
};
}
function start() {
client = new WebSocket(URL);
client.onopen = () => {
isConnected = true;
stateChangeListeners.forEach(fn => fn(true));
}
const close = client.close;
// Close without reconnecting;
client.close = () => {
reconnectOnClose = false;
close.call(client);
}
client.onmessage = (event) => {
messageListeners.forEach(fn => fn(event.data));
}
client.onerror = (e) => console.error(e);
client.onclose = () => {
isConnected = false;
stateChangeListeners.forEach(fn => fn(false));
if (!reconnectOnClose) {
console.log('ws closed by app');
return;
}
console.log('ws closed by server');
setTimeout(start, 3000);
}
}
start();
return {
on,
off,
onStateChange,
close: () => client.close(),
getClient: () => client,
isConnected: () => isConnected,
};
}
const client = reconnectingSocket(URL);
function useMessages() {
const [messages, setMessages] = useState([]);
useEffect(() => {
function handleMessage(message) {
setMessages([...messages, message]);
}
client.on(handleMessage);
return () => client.off(handleMessage);
}, [messages, setMessages]);
return messages;
}
export default function App() {
const [message, setMessage] = useState('');
const messages = useMessages();
const [isConnected, setIsConnected] = useState(client.isConnected());
useEffect(() => {
return client.onStateChange(setIsConnected);
}, [setIsConnected]);
useEffect(() => {
if (isConnected) {
client.getClient().send('hi');
}
}, [isConnected]);
function sendMessage(e) {
e.preventDefault();
client.getClient().send(message);
setMessage('');
}
return (
<div>
<h1>Websocket {isConnected ? 'Connected' : 'Disconnected'}</h1>
<form onSubmit={sendMessage}>
<input value={message} onChange={e => setMessage(e.target.value)} />
<button type="submit">Send</button>
</form>
{messages.map(m => <p>{JSON.stringify(m, null, 2)}</p>)}
</div>
);
}

Here is what I use:
const [status, setStatus] = useState('closing')
let socket
useEffect(() => {
if (!condition1) {
return
}
if (socketStatus == 'closing') {
connectSocket()
setSocketStatus('opening')
}
}, [socketStatus])
function connectSocket() {
socket = new WebSocket('ws://...');
socket.addEventListener('open', function (m) {
newSocket.send('...')
});
socket.onmessage = function (e) {
log(e.data)
}
socket.onclose = function (e) {
setTimeout(() => {
setSocketStatus('closing')
}, 2000);
};
socket.onerror = function (err: any) {
socket.close();
};
}

Related

React useState is not updating when set method is called in an interval

I have a websocket server that sends an object containing some hashes every 15 seconds. When the client receives a hash, I want to check with my current hash. If they differ, I want to make a call to an API to fetch new data.
The socket is working and sending the hash correctly. If the data updates on the server I get a different hash. My problem is that the hash variable I use to store the current hash is not updated correctly.
I have disabled the socket listening in my component, just to make sure that that is not the problem. Instead I have added a setInterval to mimik the socket update.
This is my code (socked code disabled but left as a comment):
import { useCallback, useEffect, useState } from "react";
import { useAuth, useSocket } from "../utils/hooks";
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState<QuestionListModel[]>([]);
const { user } = useAuth();
const { socket } = useSocket();
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response: ApiResponse | boolean = await getQuestionLists(user?.token);
if (typeof response !== "boolean" && response.data) {
setQuestionLists(response.data);
}
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = useCallback((newHash: Hash) => {
console.log("called update");
let shouldUpdate = false;
let originalHash = { ...hash };
let updatedHash = { ...newHash };
console.log("new: ", newHash);
console.log("stored: ", originalHash);
if (hash.questionList !== newHash.questionList) {
console.log("was not equal");
updatedHash = { ...updatedHash, questionList: newHash.questionList}
shouldUpdate = true;
}
if (shouldUpdate) {
console.log("trying to set new hash: ", updatedHash);
setHash(updatedHash);
fetchQuestionLists();
}
}, [hash]);
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, 15000)
}, []);
return (
<>
... Things here later ...
</>
);
};
export default Admin;
After the initial render, and waiting two interval cycles, this is what I see in the console:
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
called update
new: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
stored: {questionList: ''}
was not equal
trying to set new hash: {questionList: 'sdhfubvwuedfhvfeuvyqhwvfeuq'}
fetching new question lists
You can see that stored is empty. That leads me to believe that setHash(updatedHash); never runs for some reason. Why is that?
Having hacked about with this in codepen here: https://codesandbox.io/s/as-prop-base-forked-l3ncvo?file=/src/Application.tsx
This seems to me to be a closure issue as opposed to a React issue. If you have a look in the dev tools, you'll see the state of the component is doing what you're expecting it to. The issue is that the console log is not.
useEffect is only ever going to use an old version of update, so the console won't log what you're expecting. If you add update to the dependency array (and add a clean up so we don't end up with tonnes of intervals) you'll get what you're looking for. Can be seen in the linked codepen.
I think the issue in on this line :
socket?.on('aHash', (hash) => update(hash));
maybe when you register a listener, it keeps the first value of update only,
can you please share useSocket?
const [ hash, setHash ] = useState<Hash>({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
Include setHash in your dependency list et voilà
EDIT: Or well, you should include these dependencies in all your useCallback/useEffect hooks since the reference will be lost whenever the component updates. You always have to include all dependencies in the dependency list not to get unpredictable behavior.
use setState(prevValue => {}) to get the the preferred effect. Also, if you running in a Strict mode this will fire the setState twice.
Here is how the code should look like:
import { useCallback, useEffect, useState } from "react";
import { faker } from '#faker-js/faker';
const Admin = () => {
const [ questionLists, setQuestionLists ] = useState([]);
const [ hash, setHash ] = useState({questionList: ""});
const fetchHash = useCallback(async () => {
setHash({questionList: "sdhfubvwuedfhvfeuvyqhwvfeuq"});
}, []);
const fetchQuestionLists = useCallback(async () => {
console.log("fetching new question lists");
const response = {data: {hash: 'asdf-1234'}}
setQuestionLists(response.data);
}, [hash]);
useEffect(() => {
fetchHash();
fetchQuestionLists();
}, []);
const update = (newHash) => {
console.log("called update");
setHash(oldHash => {
console.log('old hash: ', oldHash);
console.log('new hash', newHash);
if (JSON.stringify(oldHash) !== JSON.stringify(newHash)) {
return newHash
}
})
};
/*useEffect(() => {
socket?.on('aHash', (fetchedHash) => update(fetchedHash));
}, []);*/
useEffect(() => {
setInterval(() => {
update({questionList: faker.random.numeric(36)});
}, 15000)
}, []);
return (
<>
<h2>Hash</h2>
{JSON.stringify(hash)}
</>
);
};
export default Admin;
In both cases (socket & interval) the issue is that you need to re-define the callback functions with the new context of the variables in the scope, whenever something changes. In this case you will probably need to put "update" (and whatever other variable you need to "watch") inside the dependancy array of the useEffect.
Ive had a similar issues. Here is how I ended up defining socket callback that updates correctly. Notice that I added the save function (just a function that saves the state into the useState). Also, you need to return a clean up function to turn the socket callback off when the component unmounts. This way every time anything changes in the dependancy array, the hook re-runs and recreates that callback with the new information.
React.useEffect(() => {
socketRef?.current?.on(
'private_message_sent_to_client',
(data: IMessageResult) => {
savePrivateMessages(data);
}
);
return () => {
socketRef?.current?.off('private_message_sent_to_client');
};
}, [meta, selectedChatId, savePrivateMessages]);
And here is an example for you
React.useEffect(() => {
socket?.on('aHash', (hash) => update(hash));
return () => {
socket?.off('aHash')
};
}, [update, hash]);

React Hook setstate runs twice

i have a function that gets called by a socket.io's event:
const [room, setRoom] = useState({})
const [messages, setMessages] = useState([])
function reciveMessage(message) {
if (room.id == message.roomid) {
setMessages(messages => [...messages, message])
}
}
the room state value is stale
if i try to work around it by
setRoom(room => {
if (room.id == event.data.source.conId) {
setMessages(messages => [...messages, message])
}
return room
})
the room value is the lastest but setMessage runs twice is there a better way?
A socket message is a side-effect, so handle it in useEFfect.
useEffect(() => {
// do whatever it is to set up the message subscription/connection
// and call `setMessages([...messages, message])` when
// one is received in the handler/callback
return () => {
// clean-up, e.g., closing connection, etc.
}
},
// will only re-run the effect if the id changes
[room.id])
setMessages will only be called when a message is received. If the room.id changes, then the useEffect code will be run again, subbing to the new room.

Data coming in from socket doesn't update the state with useEffect

See demo here
I'm connecting to sockets (modelled by setTimeouts!) and getting an array. On mount I get an initial array. Then I keep listening for updates to the array. An update is sent as just the change and not a whole array.
I need to access the current state, but it's empty. Even though it looks fine in the render.
I think this might be a scoping or closure bug caused by numbers being empty at the time of calling addLater(), but I'm not sure what the solution is.
import React, { useEffect, useState } from "react";
import "./styles.css";
export default function App() {
const [numbers, setNumbers] = useState([]);
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"]);
// incoming messages from socket
addLater();
addMuchLater();
}, []);
const addLater = () => {
window.setTimeout(() => {
console.log("Why is the state empty? ", numbers);
const changedNumbers = [...numbers];
changedNumbers.splice(1, 1, "fourth");
setNumbers(changedNumbers);
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
const changedNumbers = [...numbers];
changedNumbers.splice(2, 1, "fifth");
setNumbers(changedNumbers);
}, 10000);
};
return (
<div className="App">
{numbers.map((r, i) => (
<p>
{i}: {r}
</p>
))}
</div>
);
}
When the next value depends on the previous one it's best to write the code as a functional update so the code will always be acting on the latest value. As you ran into, your current code closes over the original value of numbers, which isn't what you want:
const addLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(1, 1, "fourth");
return changedNumbers;
});
}, 5000);
};
const addMuchLater = () => {
window.setTimeout(() => {
setNumbers(prevNumbers => {
const changedNumbers = [...prevNumbers];
changedNumbers.splice(2, 1, "fifth");
return changedNumbers;
});
}, 10000);
};
setState is asynchronous so the calls to state that happen in the functions later on get the state as it was when the component rendered, not as it is after the new state was set. You can use a callback function as the second argument:
useEffect(() => {
// initial connection to socket
setNumbers(["first", "second", "third"],
()=>{
// incoming messages from socket
addLater();
addMuchLater();
}), []);
See:
https://upmostly.com/tutorials/how-to-use-the-setstate-callback-in-react

Why using useRef in this react example? just for concept demostration?

Just wonder what purpose the useRef serve here in example: https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables:
function Timer() {
const intervalRef = useRef();
useEffect(() => {
const id = setInterval(() => {
// ...
});
intervalRef.current = id;
return () => {
clearInterval(intervalRef.current);
};
});
// ...
function handleCancelClick() {
clearInterval(intervalRef.current);
}
// ...
}
I tried and can achieve the same without useRef as below:
function Timer() {
const interval = null;
useEffect(() => {
const id = setInterval(() => {
// ...
});
interval = id;
return () => {
clearInterval(interval);
};
});
// ...
function handleCancelClick() {
clearInterval(interval);
}
// ...
}
So the saying "but it’s useful if we want to clear the interval from an event handler" from the react doc and this answer: Is useRef Hook a must to set and clear intervals in React?, just mean almost nothing at all.
It's fine only if you don't want stopping timer in handleCancelClick and keep all logic inside single useEffect(which would be really rare case).
See, if you get any re-render(because of any useState entry changed or props changed) between running timer and handleCancelClick you will get that variable const interval = null; and nothing will happen on click(clearTimeout(null); does nothing).
Don't see how that can be handled without preserving data between renders.

How remember that a React hooks component is unmounted, so can avoid state changes?

With old class based React.js code, I can do like this, to remember that a component has been unmounted:
componentWillUnmount: function() {
this.isGone = true;
},
loadUsers: function() {
Server.loadUsers(..., (response) => {
if (this.isGone) return;
...
});
}
How can one do the same thing, with hooks based components?
Here's a sample hooks function component, where I'm unsure about how to remember that it's been unmounted, so I can return before calling any setSomeState:
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
let isGone = false; // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (/* is unmounted ?? */) return;
setMembers(response.members);
});
return () => { /* how remember is unmounted?
would isGone = true; work? */ };
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (/* is unmounted ?? */) return;
setMembers([]);
});
}});
I suppose I cannot use const [isGone, setGone] = useState(false) because I shouldn't try to access the state (read isGone) after has been unmounted. And if I [222] add a local let isGone = false inside the function, it seems to me that various callbacks created inside the function, will refer to different "instances" of this local variable, depending on in which different GroupMembers(..) invokation the different callbacks were created? or am I mistaken and this works? — Maybe I could create an outer wrapper function with a local let isGone = false, however, this adds another wrapping function and indentation :- /
Since you are using the isGone flag outside of the useEffect method too, you can make use of useRef to store variables like
export const GroupMembers = React.createFactory(function(props) {
const [membersOrNull, setMembers] = React.useState(null);
const isGone = useRef(false); // ? (se [222] below
React.useEffect(() => {
Server.listGroupMembers(someGroupId, (response) => {
if (isGone.current) return;
setMembers(response.members);
});
return () => {
isGone.current = true;
};
}, []);
...
return ..., Button({ title: "Remove all members", onClick: () => {
Server.removeAllMembers(someGroupId, () => {
if (isGone.current) return;
setMembers([]);
});
}});
PS: A better way to handle such things is to cancel the request when
you are leaving the page instead of waiting for the response only to
neglect it.
just use local variable accessed through closure
React.useEffect(() => {
let isActual = true;
Server.listGroupMembers(someGroupId, (response) => {
if (!isActual) return;
setMembers(response.members);
});
return () => {isActual = false;};
}, []);
in this case flag would be updated on unmounting only. but in general case(with some unempty dependencies for useEffect) it will use as well. So in case of sequential renderings you could be sure you never process older request.
PS most universal way is cancelling request when it's possible.

Resources