React Hook setstate runs twice - reactjs

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.

Related

How to make notification appear in 4 seconds, but avoid it if state has changed

I want to show notification message after 4 seconds when some state is invalid. But if during that 4 seconds it has changed and is valid now - I want to put condition in setTimeout that would check it. But the problem is that it still uses the first state value, not the changed one. One of my assumptions to fix it was making setState in the line before synchronous, but don't know how. Maybe any other ways to fix it?
useEffect(async () => {
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
}, [beginRow, endRow]);
First You can not call useEffect callback as async method.
Second for your purpose you can act as below:
let timeoutId = null;
useEffect(() => {
(async () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
try {
const snippetIndexResponse = await getSnippetIndex(
//some params
);
if (snippetIndexResponse !== -1) {
setSnippetIndex(snippetIndexResponse);
} else {
setSnippetIndex(null)
timeoutId = setTimeout(() => {
console.log(snippetIndex) <-- it logs only first state, instead wanted null
if(!snippetIndex) {
openNotificationWithIcon(
"error",
"Invalid snippet selection",
"Snippet slice shouldn't tear code blocks. Please, try again."
);
}
}, 4000)
}
} catch (err) {
setSnippetIndex(null);
openNotificationWithIcon("error", err.name, err.message);
}
})();
}, [beginRow, endRow]);
I think you could make the notification UI a component and pass the state in as a parameter. If the state changes the component will be destroyed and recreated.
And you can add the 4 second timer in useEffect() as well as cancel it in the 'return' of useEffect. If the timer fires, update some visibility flag.
Live typing this - so may contain some syntax errors...
cost myAlert = (isLoading) => {
const [isVisible, setIsVisible] = setIsVisible(false)
useEffect(()=>{
const timerId = setTimer, setIsVisible(true) when it goes off
return ()=>{cancelTimer(timerId)}
}, [isLoading, setIsVisible])
if (!isLoading && !isVisible) return null
if (isVisible) return (
<>
// your ui
</>
)
}
You may want to useCallback on setTimer so it won't cause useEffect to fire if isLoading changes -- but I think this should get you close to the solution.

How to send last debounced request while request param is changing?

I was using React.useCallBack() to produced a debounced function, something like:
const debouncedFunction = React.useCallback(
_.debounce(function, 2000),
[someInputValue]
)
and trigger it using React.useEffect(), like:
React.useEffect(() => {
debouncedFunction();
},[someInputValue])
When I change someInputValue(like typing "hi" in a Input component) in a short time(less than 2 seconds), 2 requests were sent, one with param "h" and one with param "hi".
I understand this was due to debouncedFunction was recreated every time someInputValue was changed, so multiple debounced requests cannot be merged into a single one. So what should I do to send one "merged" request with param "hi"?
Without using an extra lib:
const useDebounce = (value, delay, fn) => { //--> custom hook
React.useEffect(() => {
const timeout = setTimeout(() => {
fn();
}, delay);
return ()=> {
clearTimeout(timeout);
}
}, [value]);
};
SomeComponent.js
const [someInputValue, setsomeInputValue] = useState(null);
const fn = ()=> console.log(someInputValue);
useDebounce(someInputValue, 2000, fn);

Can't run functions in .then() Axios React

I have a webpage where I fetch the data with async axios and then make calculations with them.
Here is the code snippet:
const FetchData = async () =>{
console.log("FETCH CALLED");
await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
.then(resp => {
setStockData(resp.data);
calculateTrend();
calculateTrendDirection();
})
}
Here, I get the error at calculateTrend() function. My question is, that this .then() should run when the response has arrived, but it seems that it runs before. Because both calculateTrend and calculateTrendDirection works with this fetched data
Edit: The error I am getting is Cannot read property 'previousClosePrice' of undefined. I am sure this exist in the object so mispelling is not a problem
Edit2: I edited my Component according to your solutions and one happens to work, the only thing is that the fetching gets to an infinite loop and fetches multiple times a second. My suspect is the dependencies in useEffect, but I am not sure what to set there.
Here is my full component:
function StockCard(props) {
const [FetchInterval, setFetchInterval] = useState(300000);
const [StockData, setStockData] = useState({});
const [TrendDirection, setTrendDirection] = useState(0);
const [Trend, setTrend] = useState(0);
const FetchData = async () =>{
console.log("FETCH CALLED");
const resp = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`)
setStockData(resp.data);
}
const calculateTrendDirection = () => {
console.log(StockData.lastPrice);
if(StockData.lastPrice.currentPrice > StockData.lastPrice.previousClosePrice){
setTrendDirection(1);
} else if (StockData.lastPrice.currentPrice < StockData.lastPrice.previousClosePrice){
setTrendDirection(-1);
} else {
setTrendDirection(0);
}
}
const calculateTrend = () => {
console.log(StockData.lastPrice);
var result = 100 * Math.abs( ( StockData.lastPrice.previousClosePrice - StockData.lastPrice.currentPrice ) / ( (StockData.lastPrice.previousClosePrice + StockData.lastPrice.currentPrice)/2 ) );
setTrend(result.toFixed(2));
}
useEffect(() => {
FetchData();
if(StockData.lastPrice){
console.log("LÉTEZIK A LAST PRICE")
calculateTrend();
calculateTrendDirection();
}
const interval = setInterval(() => {
FetchData();
}, FetchInterval)
return() => clearInterval(interval);
},[StockData, FetchData, FetchInterval, calculateTrend, calculateTrendDirection]);
return(
<div>
<CryptoCard
currencyName={StockData.lastPrice? StockData.name : "Name"}
currencyPrice={StockData.lastPrice? `$ ${StockData.lastPrice.currentPrice}` : 0}
icon={<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Bitcoin.svg/2000px-Bitcoin.svg.png"/>}
currencyShortName={StockData.lastPrice? StockData.symbol : "Symbol"}
trend={StockData.lastPrice? `${Trend} %` : 0}
trendDirection={StockData.lastPrice? TrendDirection : 0}
chartData={[9200, 5720, 8100, 6734, 7054, 7832, 6421, 7383, 8697, 8850]}
/>
</div>
)
The then block is called only after the promise is fulfilled, so the data is available at that point.
From what I can see, the problem is setStockData tries to set the stockData state variable with the response, but calculateTrend and calculateTrendDirection are called before the state is set because updating state values is batched.
There are several solutions to the problem.
Solution 1:
You can call the two functions after the state is set:
setStockData(resp.data, () => {
calculateTrend();
calculateTrendDirection();
});
Solution 2:
You can use useEffect to call the functions again after the state is updated:
useEffect(() => {
if (stockData) { // or whatever validation needed
calculateTrend();
calculateTrendDirection();
}
}, [stockData]);
Solution 3:
You can pass the parameters to the method:
calculateTrend(resp.data);
calculateTrendDirection(resp.data);
The best option? I think #2, because it also makes sure that the trend and trend direction are re-calculated whenever stock data is updated (from whatever other causes).
I guess in calculateTrend you are using the data which setStockData sets to the state, if that is the case
setState is not happening right after you call the setState, if you want something to execute after correctly update the State then should look at something like this
setStockData(resp.data, () => {
calculateTrend();// this will call once the state gets changed
});
or you could use useEffect
useEffect(() => {
calculateTrend(); // this will call every time when stockData gets changed
}, [stockData])
If you are using stockData inside calculateTrend function and setStockData is an async function, move calculateTrend function to useEffect using stockData as dependency, so every time stockData is updated, calculateTrend and calculateTrendDirection will be called:
useEffect(() => {
const interval = setInterval(() => {
FetchData();
}, FetchInterval);
return() => clearInterval(interval);
}, [FetchInterval]);
useEffect(() => {
if(StockData.lastPrice){
console.log("LÉTEZIK A LAST PRICE")
calculateTrend();
calculateTrendDirection();
}
}, [StockData]);
const FetchData = async () =>{
console.log("FETCH CALLED");
const res = await Axios.get(`http://localhost:8080/stock/getquote/${props.API}`);
setStockData(resp.data);
}

Reconnecting web socket using React Hooks

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();
};
}

Any issues using useEffect this way?

Is there any potential issues handling componentDidMount and componentDidUpdate with useHooks in this manner?
Two goals here:
Use one useEffect to handle both componentDidMount and componentDidUpdate
No need to pass in the 2nd argument (normally an array with props)
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
}) // <- no need to pass in 2nd argument
There is no issue with it but if any props or state value will change then useEffect will not load on component re-render.
so better to use 2nd argument when this depends on the values change.
const once = useRef(false)
useEffect(() => {
if(once.current === false){
once.current = true
// do things as were in componentDidMount
return
}
// do things as were in componentDidUpdate
// clean up
return () => {
//
}
},[]) // some value as array who will change and this need to be called
Yopu should pass an empty array as an argument
useEffect(() => {
if(once.current === false){
// do things as were in componentDidMount
once.current = true
}
return () => {
// this area will fired when component unmounts
}
}, []) // by providing an empty array this useEffect will act like componentDidMount
If you want to re-render useEffect on a state change you can add the state to the empty array so it will listen changes in the state and will be executed
const [someStateValue, setSomeStateValue] = useState('')
useEffect(() => {
...
}, [someStateValue]) // will be executed if someStateValue changes
If you want to combine componentDidMount and componentDidUpdate, I have a solution in my mind that may work
const [val, setVal] = useState('asd')
const [oldVal, setOldVal] = useState('')
useEffect(() => { // componentDidMount
if(val !== oldVal){ // componentDidUpdate
// pass val to old val
setOldVal(val)
// set new val to new val
setVal("theNewestVal")
}
return () => { // componentWillUnmount
...
}
}, [val]) // will be executed if someStateValue changes

Resources