React hooks: accessing up-to-date state from within a callback

后端 未结 8 1765
梦谈多话
梦谈多话 2020-12-25 09:35

EDIT (22 June 2020): as this question has some renewed interest, I realise there may be a few points of confusion. So I would like to highlight: the example in the question

8条回答
  •  醉酒成梦
    2020-12-25 10:23

    I would use a combination of setInterval() and useEffect().

    • setInterval() on its own is problematic, as it might pop after the component has been unmounted. In your toy example this is not a problem, but in the real world it's likely that your callback will want to mutate your component's state, and then it would be a problem.
    • useEffect() on its own isn't enough to cause something to happen in some period of time.
    • useRef() is really for those rare occasions where you need to break React's functional model because you have to work with some functionality that doesn't fit (e.g. focusing an input or something), and I would avoid it for situations like this.

    Your example isn't doing anything very useful, and I'm not sure whether you care about how regular the timer pops are. So the simplest way of achieving roughly what you want using this technique is as follows:

    import React from 'react';
    
    const INTERVAL_FOR_TIMER_MS = 3000;
    
    export function Card({ title }) {
      const [count, setCount] = React.useState(0)
    
      React.useEffect(
        () => {
          const intervalId = setInterval(
            () => console.log(`Count is ${count}`),
            INTERVAL_FOR_TIMER_MS,
          );
          return () => clearInterval(intervalId);
        },
        // you only want to restart the interval when count changes
        [count],
      );
    
      function clickHandler() {
        // I would also get in the habit of setting this way, which is safe
        // if the increment is called multiple times in the same callback
        setCount(num => num + 1);
      }
    
      return (
        
    Active count {count}
    ); }

    The caveat is that if the timer pops, then you click a second later, then the next log will be 4 seconds after the previous log because the timer is reset when you click.

    If you want to solve that problem, then the best thing will probably be to use Date.now() to find the current time and use a new useState() to store the next pop time you want, and use setTimeout() instead of setInterval().

    It's a bit more complicated as you have to store the next timer pop, but not too bad. Also that complexity can be abstracted by simply using a new function. So to sum up here's a safe "Reacty" way of starting a periodic timer using hooks.

    import React from 'react';
    
    const INTERVAL_FOR_TIMER_MS = 3000;
    
    const useInterval = (func, period, deps) => {
      const [nextPopTime, setNextPopTime] = React.useState(
        Date.now() + period,
      );
      React.useEffect(() => {
        const timerId = setTimeout(
          () => {
            func();
            
            // setting nextPopTime will cause us to run the 
            // useEffect again and reschedule the timeout
            setNextPopTime(popTime => popTime + period);
          },
          Math.max(nextPopTime - Date.now(), 0),
        );
        return () => clearTimeout(timerId);
      }, [nextPopTime, ...deps]);
    };
    
    export function Card({ title }) {
      const [count, setCount] = React.useState(0);
    
      useInterval(
        () => console.log(`Count is ${count}`),
        INTERVAL_FOR_TIMER_MS,
        [count],
      );
    
      return (
        
    Active count {count}
    ); }

    And as long as you pass all the dependencies of the interval function in the deps array (exactly like with useEffect()), you can do whatever you like in the interval function (set state etc.) and be confident nothing will be out of date.

提交回复
热议问题