React - use Dependencies in useEffect without trigger the effect itself when the Dependency change - reactjs

I have a react state
const [list, setList] = useState([])
and a react effect that is triggered when the list is modified, and do some work with the first element in the list:
useEffect( () => {
if(list.length <= 0) return
//Do something with the first element of the list
//Remove first element of the list
}, [list])
In this way, the effect trigger itself n times where n is the number of the element in the list.
Then i have another method in my component that insert elements in the list with the setList() method, let's call it
insertElemInList = () => {
//insert one or more elements in list
}
when insertElemInList is called, the useEffect trigger and start working for n times.
I don't know how many times the insertElemInList() is called, and how many elements is inserted every time, since this method is called after some actions of the user on the page.
So if an user call the insertElemInList() two or more times, before the last iteration of the effect is finished, then the effect trigger in the wrong way, in fact it will activate due to the change of state given by insertElemInList, but also by itself, resulting in more iterations and wrong behaviour.
So i'm trying to figure out how to use something inside the effect that doesn't trigger the effect itself, but can be used correctly.
for example I was thinking of modifying the effect and the state adding
const [semWait, setSem] = useState(1)
and then, continue to update the list state with the insertElemInList() method, but now:
useEffect( () => {
let doSomething = () => {
if(list.length < 0) return
//Do Something with the first element of the list
//Remove first element from the list
if(list.length > 0) doSomething()
}
doSomething()
setSem(1)
}, [semWait])
insertElemInList = () => {
//insert one or more elements in list
if(semWait == 1) setSem(0)
}
the above code is just an example of how I can solve the problem, I don't think it is the best solution and I gave you this example just to make you understand what I would like to do.
however, as you can see in this way I could add as many value as i want to my state whith insertElemInList() ​​and trigger the effect only if it is not already active (in other word, only if the semaphore is reset by the effect itself). However, I know it's not a good thing to use a state in the effect, without including it in dependencies, and if i add the state list as dependency of the useEffect the problem return.
the problem is that I can't figure out how to use a value inside useEffect without including it in the dependency
EDIT:
sorry for the late reply, i tried to implement this code on my own but there are workflow problems in my work, i'll try to explain the problems:
the code is a snippet to download some file from an API, the user on the site have a list of files to download, he can click on the files to download them as many times as he wants. my intent is to create a request queue, so as not to send too many requests to the server.
the code below show my work, i've inserted some comments to let you figure out how my code should work:
const [queue, setQueue] = useState({
"op_name": "NO_OP"
})
//file download function
let requestFileDownload = (fileId) => {
/*
This function construct the object to put in queue state and call the method 'insertInQueue'
*/
let workState = appState
insertInQueue({
"op_name": "DOWNLOAD_FILE",
"file_id": fileId,
"username": workState.user.username,
"token": workState.user.token
})
}
//Function insertInQueue to insert an element in the queue
let insertInQueue = (objQueue) => {
//Some control to check if the object passed exist, and have valid fields
if (!objQueue || !objQueue.op_name || objQueue.op_name === "NO_OP") return //nothing to insert in queue
//calling method to insert in timeline div, this work only whith front-end dom elements (full synchronous)
insertElemInTimeline(objQueue.op_name)
//setting timeout in which try to insert the object passed in queue
setTimeout(function run() {
let workQueue = queue //gettind queue object
if (workQueue && workQueue.op_name === "NO_OP") {
/*
if queue exist and the op_name is "NO_OP", this mean that the previus operations on the queue
is finished, so we can start this operation
*/
setQueue(objQueue) //set the queue with the object passed as paramether to trigger the effect
return;
}
// if the queue op_name was != "NO_OP" call the function again for retry to insert in 1 second
setTimeout(run, 1000)
}, 0)
}
//Effect triggered when queue object change
useEffect(() => {
if (queue.op_name === "NO_OP") return //no operation to do
//Effective file download
let downloadFileEffect = async () => {
let objQueue = queue //getting queue state
//Two functions to download the element by calling backend api
let downloadFileResponse = await downloadFile(objQueue.file_id, objQueue.username, objQueue.token)
download(downloadFileResponse.data, downloadFileResponse.headers['x-suggested-filename'])
//after the method have completed, i can set a new state for the queue with "op_name": "NO_OP"
let appoStateQueue = {
"op_name": "NO_OP"
}
setQueue(appoStateQueue)
//method for remove the element from the dom
removeElemFromTimeline()
}
//calling function to trigger the donwload.
downloadFileEffect()
}, [queue])
now the problem is, that when i try to reset the queue state in the effect, when i call:
let appoStateQueue = {
"op_name": "NO_OP"
}
setQueue(appoStateQueue)
the queue is not resetted in the case the user have clicked two download one after the first is running.
In fact the queue stops with the first object inserted in it, and is not reset by the effect, so the second download never starts, because it sees forever the queue occupied by the first download.
In case user click one download, then wait for the download, and only then click the second, then there's no problem, and the queue is resetted correctly by the effect

First, useEffect doesn't run for "trigger itself n times where n is the number of the element in the list". useEffect will run every time list changes in length or resides in a different memory space than it did in a previous render. This is how "shallow" comparison works with javascript objects in react. Your main issue is that you are changing your dependency from within the effect. This means that while the effect runs, it updates the dependency and forces it to run again and again and again...memory leak!
Your solution might work, but as you stated is not best practice. A better solution (imo) would be to allow for a "parsedList" state that can be the end result of parsing the list. Let the source of truth with the list only be impacted by the client interaction. You monitor these changes and change your parsedList based on these changes.

Related

After sending each message there becomes 2 more messages using socket io

I have a React website.
I receive messages like this:
useEffect(() => {
socket.on('message', message => {
console.log(message)
})
}, [socket])
I send messages like this:
socket.emit('chatMessage', { message, id })
Server side:
socket.on('chatMessage', ({ message }) => {
socket.broadcast.emit('message', message)
})
First time there is 2 message (1 for the user who sent it), the next time there is 4, 6, 8 and so on.
Cleaning up the connections from the previous renders
useEffect(() => {
let isValidScope = true;
socket.on('message', message => {
console.log(message)
// if message received when component unmounts
// stop executing the code
if (!isValidScope) { return; };
// if you need to access latest state, props or variables
// without including them in the depedency array
// i.e you want to refer the variables without reseting the connection
// use useRef or some custom solution (link below)
})
return () => {
// cleanup code, disconnect
// socket.disconnect()
isValidScope = false;
}
}, [socket])
more about useEffect life cycle, to get an idea why
A new effect is created after every render
How the cleanup for previous effect occurs before executing of current useEffect
You can read about why isValid is set synchronizing with effects
Why it was running 3 times in dev mode
If you are intererested in taking a deep dive, consider reading a blog post by Dan on useEffect, its old but helps to build a good mental model about useEffects and functional components.
useEvent can solve the problem but it is in RFC
you can check my question about a implementation to build a custom useEvent till it becomes stable
Hope it helps, cheers
The problem with your code is you assume your component will never be recreated. But React does not provide such guarantees. And if you will add logging at the place when you open a socket, you will notice that for the first render it will be called 2 times. And because you do not have cleanup code the socket remains open even after the component is destroyed. Thus duplicated messages.
Furthermore it would seem that your component is recreated on every message, which multiplies the existing effect of duplication.
The solution in your case would be to close the connection in cleanup part of the effect.

ReactJS: changes in state not recognized inside functions?

Suppose I have a recursive function that increments a counter, and I'd like to reset this counter upon a user request.
For this, I declared a state variable that would get 'true' when the user requests a reset.
Inside my function, I have a condition checking whether this state variable is true, and if so, does what it does to reset the timer.
Unfortunately, the condition never becomes true as the state change isn't recognized (I double-checked it using the console to make sure).
The same code does work when using a global variable like window.resetRequested, or by declaring a variable outside the component function, instead of state.
I feel like there's something basic I'm missing here (which makes sense as I'm pretty new to web programming).
let timerValue = currentUser.sessionTimeout;
function createTimer() {
if (timerResetRequested === true) {
timerValue = currentUser.sessionTimeout;
}
if (timerValue === 0) {
const logOutDate = new Date();
logOut();
}
else {
setTimeout(() => {
timerValue --;
createTimer();
}, 1000);
}
}
createTimer();
The above is pretty much what I'm trying to do.
Thanks for any help.
I am not too familiar with
let timerResetRequested = currentUser.sessionTimeout;
However, it looks like you are not interacting with the state at all which is why you are not seeing a state change.
Typically you would declare a state value:
const [timeValue, setTimeValue] = useState(0)
Then you could implement a function which increases that state value by clicking a button, for example.
then to reset the value of the timer you would implement a reset function which would reset the state value to zero.
I hope this makes sense?

React log final state value after delay

I have a MERN app. On the react side, I have a state. This state may or may not change many times a second. When the state is updated, I want to send the state to my back end server API so I can save the value in my mongodb. This state can possibly change hundreds of times a second which I wish to allow. However, I only want to send this value to my server once every 5 seconds at most. This is to avoid spam and clogging my mongodb Atlas requests.
Currently, I have tried setInterval, setTimeout and even locking cpu with a while(time<endTime).
These have all posed an issue:
The setInterval is nice since I could check the currentValue with the lastSentValue and if they do not equal (!==) then I would send the currentValue to my server. Unfortunately, when I set interval, it returns the initial value that was present when the setInterval was called.
If you know how I can let a user spam a boolean button while only sending updates at most once every 5 seconds from the front end (React) to the back end (Node) and that it sends the current and up to date value then please share your thoughts and I will test them as soon as possible.
My state value is stored as::
const [aValue, anUpdate] = useState(false);
The state is changed with an onClick method returned in my React app.
function Clicked(){
anUpdate(!aValue);
}
My set interval test looked like this::
//This is so that the button may be pressed multiple times but the value is only sent once.
const [sent, sentUpdate] = useState(false);
//inside of the clicked method
if(!sent){
sentUpdate(true);
setInterval(()=>{
console.log(aValue);
},1000);
}
My setTimeout is very similar except I add one more sentUpdate and reset it to false after aValue has been logged, that way I can log the timeout again.
//setInterval and setTimeout expected results in psudocode
aValue=true
first click->set aValue to !aValue (now aValue=false), start timeout/interval, stop setting timeouts/interval until completed
second click->set aValue to !aValue (now aValue=true), do not set timeout/interval as it is still currently waiting.
Completed timeout/interval
Log->false
//expected true as that is the current value of aValue. If logged outside of this timeout then I would receive a true value logged
In quite the opposite direction, another popular stackOverflow answer that I stumbled upon was to define a function that used a while loop to occupy computer time in order to fake the setTimeout/setInterval.
it looked like this::
function wait(ms){
let start = new Date().getTime();
let end = start;
while(end < start + ms) {
end = new Date().getTime();
}
}
Unfortunately, when used inside of the aforementioned if statement (to avoid spam presses) my results were::
aValue=true
first click->set aValue to !aValue (now aValue=false), start wait(5000), turn off if statement so we don't call many while loops
second click->nothing happens yet - waiting for first click to end.
first click timeout->logged "false"->if statement turned back on
second click that was waiting in que is called now->set aValue to !aValue (now aValue=true), start wait(5000), turn off if statement so we don't call many while loops
second click timeout->logged "true"->if statement turned back on
So the while loop method is also not an option as it will still send every button press. It will just bog down the client when they spam click.
One more method that I saw was to use a Promise to wrap my setTimeout and setInterval however that in no way changed the original output of setTimeout/setInterval.
It looked like this::
const promise = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(true);
},5000);
});
promise.then(console.log(aValue));
//I also tried resolve(aValue)->promise.then(val=>console.log(val));
For what you are trying to do with looping and starting intervals in the callback will only ever close over a specific state value fo reach iteration, i.e. the initial state value.
The solution is to use an useEffect hook to handle or "listen" for changes to a dependency value. Each time the state updates a component rerender is triggered and the useEffect hook is called, and since the dependency updated, the hook's callback is called.
useEffect(() => {
sendStateToBackend(state);
}, [state]);
If you want to limit how often sendStateToBackend is actually invoked then you want to throttle the call. Here's an example using lodash's throttle Higher Order Function.
import { throttle } from 'lodash';
const sendStateToBackend = throttle((value) => {
// logic to send to backend.
}, 5000);
const MyComponent = () => {
...
useEffect(() => {
sendStateToBackend(state);
}, [state]);
...
Update
If you want to wait until the button is clicked to start sending updates to the backend then you can use a React ref to track when the button is initially clicked in order to trigger sending data to backend.
const clickedRef = React.useRef(false);
const [aValue, anUpdate] = React.useState(false);
React.useEffect(() => {
if (clickedRef.current) {
sendToBackend(aValue);
}
}, [aValue]);
const clicked = () => {
clickedRef.current = true
anUpdate(t => !t);
};
...
See also Lodash per method packages since package/bundle size seems a concern.
So I had a brain blast last night and I solved it by creating a series of timeouts that cancel the previous timeout on button click and set a new timeout with the remaining value from the last timeout.
const [buttonValue, buttonUpdate] = useState(props.buttonValue);
const [lastButtonValue, lastButtonUpdate] = useState(props.buttonValue);
const [newDateNeededValue, newDateNeededUpdate] = useState(true);
const [dateValue, dateUpdate] = useState();
const [timeoutValue, timeoutUpdate] = useState();
function ButtonClicked(){
let oldDate = new Date().getTime();
if(newDateNeededValue){
newDateNeededUpdate(false);
dateUpdate(oldDate);
}else{
oldDate = dateValue;
}
//clear old timeout
clearTimeout(timeoutValue);
//check if value has changed -- value has not changed, do not set timeout
if(lastButtonValue === !buttonValue){
console.log("same value do not set new timout");
buttonUpdate(!buttonValue);
return;
}
//set timeout
timeoutUpdate(setTimeout(()=>{
console.log("timed out");
lastButtonUpdate(!buttonValue);
newDateNeededUpdate(true);
//This is where I send to my previous file and send to my server with axios
props.onButtonUpdate({newVal:!buttonValue});
clearTimeout(timeoutValue);
}, (5000-(new Date().getTime() - oldDate))));
}

useState and useRecoilState return undefined when try to set a value string on API response

I'm experiencing a big problem with useState and useRecoilState.
In particular, I make a call to a rest api with DELETE method, this call returns me a jobId to be able to poll in the future in order to know the status of this job.
The call is successful and the response returns correctly, however when I try to set the "ReturnJobId" status it becomes undefined. No console errors or warnings are returned.
I've tried using both the react state and the recoil state and they both have the same result.
I expect the state to be set correctly as it is a simple string. Below is the offending piece of code.
As you can see from the code the variable in question "returnedJobId" is initialized to null but after trying to set it becomes undefined.
It is not even a timing problem, I tried to make console log after many seconds and the status still remains undefined.
Thanks in advance to anyone who tries to help me
const [returnedJobId, setReturnedJobId] = useState(null);
fetch(basePath + "/digital/cpes/" + cpeToReset + "/redreset", {
method: 'DELETE',
headers: {
federationId: userFederationId
}
}).then(res => res.json())
.then(data => {
console.log("Dati ordine di reset: ", data);
if (data.code !== 200) {
setIsResetting(false);
console.log("Response code errato: ", data.code);
} else {
console.log("Ordine di reset OK");
setReturnedJobId(data.jobId); /*This is the problematic instruction */
console.warn('Setto il seguente job id: ', data.jobId);
setTimeout(() => {
console.log("Stato del job id: ", returnedJobId); /*Here is undefined*/
}, 1000);
}
})
I'm assuming returnedJobId is the value corresponding to the setReturnedJobId state updater. I'm also assuming you're logging the state within a setTimeout because you're aware that state updates to not take place immediately. The reason you're still having a problem is due to a stale closure.
Notice that if you declared state correctly, returnedJobId is a const. A const cannot change. Even though you've delayed the logging of the variable, it is still a const and will never be updated.
The way useState works is by capturing the value of what the new state should be on the next render. In a functional component, the next render corresponds to the next time the functional component is called by React. When any function is called (component or otherwise) all local variables are brand new. The previous render's local variables are destroyed, and new ones are created in their place. useState is so useful because it assigns that new const the value you passed to the updater function. It has the illusion of updating the const, but in reality it is a completely new variable.
So no matter what, you cannot get the new value of state during the current render. You must wait until the next one. The most common way to do this is to use a useEffect.
useEffect(() => {
console.log("Stato del job id: ", returnedJobId);
}, [returnedJobId]);

Determine which hooks are being called

If I have some state setup with useState, such as:
const [s, setS] = React.useState();
and I want to put some sort of logging on the middleware, for instance, to log when each call to setS is made, I can do something a bit hacky, like this:
const [s, setS_] = React.useState();
const setS = x => {
console.log('setS called with: ');
console.log(x);
setS(x);
}
But this gets a little unwieldy with lots of items of state. Is it possible to do this in a way that's avoids repetition, for instance, establishing some hook on useState?
If you're wondering why I want to do this in the first place, I have some dispatchAction's that are taking a very long time, and I'd like to try and begin debugging these; it's a little difficult as I have no way to know which actions are taking so long.
You can create a useEffect which triggers when s changes. Not sure if this satisfies all of your requirements as it won't be triggered if setS is pass the current state of s, (ie. s = 3 and setS(3) is called)
useEffect(() => {
console.log('s changed to: ', s)
}, [s])
You can wrap the original React.useState function to enable logging, but the main challenge is to figure out who's calling and for what piece of state, because there's no info passed to useState() except for an optional initial value.
How to identify the component / piece of state
One idea is to use Function.caller to identify who's calling, but it doesn't work in strict mode (and depending on how you have your build process set up, it might be enabled automatically).
An alternative is to throw, and capture, an error and look at its stacktrace:
let oldUseState = React.useState;
React.useState = function useState() {
let ctx;
try { throw new Error(); } catch(err) { ctx = err; }
let [val,updater] = oldUseState(...arguments);
return [val, function() {
console.log(arguments, ctx);
return updater(...arguments);
}];
}
Whenever the updater function for a piece of state is called, you'll get a stacktrace in the console, with the added bonus that the list of callers is cross-referenced with your code, so you can click to see what invoked the function.

Resources