React, how to check if state is remaining in certain value for certain time?

73 Views Asked by At

I have a state value, it may change from time to time. I want to have a flag that indicate "this state has been staying in value x without any changes for y amount of time"

How do I achieve this?

P.S, if you played Oxygen Not Included, this is the logic I want: https://oxygennotincluded.fandom.com/wiki/FILTER_Gate

Update: can't use setInterval/polling for this.

2

There are 2 best solutions below

1
WestMountain On

The code in this answer is available at https://stackblitz.com/edit/react-qjmyix?file=src%2FApp.js

The function useStateWithTimeFlag in the snippet below, returns an object with functions identical to those of setState, as well as a function for getting the elapsed time, and a variable with the elapsed time.
You can use whichever you prefer.

import React, { useState, useRef, useEffect, useCallback } from 'react';

const useStateWithTimeFlag = (initValue) => {
  const [value, setValue] = useState(initValue);
  const [ElapsedTime, setElapsedTime] = useState(0);
  const timeRef = useRef(new Date());

  const getTime = useCallback(() => {
    return (new Date() - timeRef.current) / 1000;
  }, []);

  useEffect(() => {
    const i = setInterval(() => {
      setElapsedTime(getTime());
    }, 1000);
    return () => {
      clearInterval(i);
    };
  }, []);

  useEffect(() => {
    timeRef.current = new Date();
  }, [value]);

  return {
    value,
    setValue,
    getTime,
    ElapsedTime,
  };
};

The first useEffect hook updates the ElapsedTime variable once every second.
The second hook resets the timer, whenever the state changes.

This components shows how the function can be used.

export default function App() {
  const { setValue, value, ElapsedTime, getTime } = useStateWithTimeFlag('');

  return (
    <div>
      <h1>Hello StackBlitz!</h1>
      <div>
        {' '}
        value: {value} stay there in {ElapsedTime} second{' '}
      </div>
      <div>
        {' '}
        value: {value} stay there in {getTime()} second{' '}
      </div>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
    </div>
  );
}

0
Soenderby On

In the following snippet I have adapted the answer given by @WestMountain to more closely imitate the functionality of the filter gate described in your link.

This component will only change the exposed value after it has remained unchanged for the amount of time specified by the delay parameter.

import React, { useState, useRef, useEffect, useCallback } from 'react';

const useStateWithTimeFlag = (initValue, delay) => {
  const [internalValue, setInternalValue] = useState(initValue);
  const [ElapsedTime, setElapsedTime] = useState(0);
  const timeRef = useRef(new Date());
  const timeoutRef = useRef(null);

  const getTime = useCallback(() => {
    return (new Date() - timeRef.current) / 1000;
  }, []);

  useEffect(() => {
    const i = setInterval(() => {
      setElapsedTime(getTime());
    }, 10);
    return () => {
      clearInterval(i);
    };
  }, []);

  useEffect(() => {
    // Reset timer when internalValue changes
    timeRef.current = new Date();
  }, [internalValue]);

  const setValue = (value) => {
    // Remove any existing timeout
    if (timeoutRef.current !== null) 
      clearTimeout(timeoutRef.current)
    // Create new timeout that will set internalValue after delay has expired
    timeout = setTimeout(function() { setInternalValue(value) }, delay);
    timeoutRef.current = timeout
  }  
  return {
    internalValue,
    setValue,
    getTime,
    ElapsedTime,
  };
};

The function to set internalValue is not exposed. It has been replaced with the function setValue that only sets the internal value after a delay has expired. If the value is changed again (by another call to setValue) before the delay has passed, the timeout is reset.

With regards to polling, I am not sure there is a good way to get around it. I do not know the context of your application, but I would suggest simply lowering the delay in setInterval.
In the snippet I have set it to 10 milliseconds which is 100 updates per second. (Note: This may impact performance)

Another option is that you could make it event based.