1

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

2 Answers2

0

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

...

Edit react-log-final-state-value-after-delay

See also Lodash per method packages since package/bundle size seems a concern.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • I'd rather not have to install another 3rd party library. I did that while creating my text boxes and ended up having to hand code it all myself after finding a ton of flaws in the library. Do you know a proper method for sending a final dynamic value after x amount of time? –  Aug 08 '21 at 05:24
  • @Karl Well, you *could* [roll your own](https://stackoverflow.com/a/27078401/8690857), but it's suggested to use a common and well tested implementation. – Drew Reese Aug 08 '21 at 05:28
  • I tried the throttle's that you linked to at "https://stackoverflow.com/a/27078401/8690857" but they did not fit my purposes. They would run my function immediately without waiting the input amount of milliseconds. Perhaps I will need to reword my query. I did not think that it was so hard to understand. –  Aug 08 '21 at 05:52
  • @Karl I don't think there is any misunderstanding of your question. You want to throttle a callback to your server to once per every 5 seconds maximum. As I said in my answer, I just provided an example that should resolve your issue. You're free to use any other throttling implementation of your choice. – Drew Reese Aug 08 '21 at 06:02
  • I do not want to throttle anything. And I would prefer not to look into very large 3rd person libraries that may or may not work. I have a button that is true or false. This button may be pressed many times. Once the button is pressed, I want it wait for a length of time. After the time I would like for it to retrieve the current value from my state and do something with it. Currently, I am trying to log the state after the timeout but I can only get React to log the original state and not the current state. I am looking into using useRef to save my timeout. –  Aug 08 '21 at 06:24
  • @Karl I don't know why you think lodash would be buggy without even testing it out, but ok, and you can import ***just*** the function you need, so size isn't an issue. That being said, then I agree now that perhaps you didn't describe well enough what your issue was and what you wanted. In your question you said "...want to send this value to my server once every 5 seconds at most", this is a throttle. ‍♂️ So I understand now, you want a button that when clicked, waits *some time* to grab the current state to do something with it? What if the user clicks again within the *some time*? – Drew Reese Aug 08 '21 at 06:44
  • @Karl Ok, I think I see the discrepancy here. With the `useEffect` the `sendToBackend` is called on the initial render when the button hasn't been clicked yet. This can be resolved easily with a ref. I'll update my answer. I do apologize for misunderstanding that part of your comment. – Drew Reese Aug 08 '21 at 07:01
0

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