React: How to implement an Interval Hook

useInterval((a)= > {
  // do something...
}, 1000);
Copy the code

React Hook You’ve probably seen and written about React Hook, but do you really know anything about hook behavior? This article will show you how to hook up class component “anomalies”.

Why implement another setInterval when you already have one

Its the arguments are “dynamic”

Notice that our setInterval accepts a dealy value, and this value is controlled by our code, which means that we can adjust this value dynamically at any time.

You can use one interval to control the speed of another

The class component implementation

First try

function Counter() {
  const [count, setCount] = useState(0);
  useEffect((a)= > {
    const id = setInterval((a)= > {
      setCount(count + 1);
    }, 1000);
    return (a)= > clearInterval(id);
  });
  return <h1>{count}</h1>;
}
Copy the code

We usually start with an implementation like useEffect interval, return cleanup. However, there is a strange aspect to writing this…

React defaults to re-executing Effects after each render, which is exactly what React expects because it avoids a whole class of bugs.

We usually use Effect to subscribe and unsubscribe some of the apis, but using setInterval can be problematic because there is a time lag between executing clearInterval and setInterval. When React renders too often, The interval never gets a chance to execute at all!

We render the Counter component at 100ms and we see that the count value is not updated

Second attempt

In the previous phase, our problem was that repeating Effects caused the interval to be cleaned up too early.

We know that useEffect can be passed a parameter to determine whether effects should be repeated. Try it out

function Counter() {
  const [count, setCount] = useState(0);

  useEffect((a)= > {
    const id = setInterval((a)= > {
      setCount(count + 1);
    }, 1000);
    return (a)= >clearInterval(id); } []);return <h1>{count}</h1>;
}
Copy the code

Ok, so now we update counter to 1 and stop

What happened? !

This is actually a very common closure problem that has its own version of Lint.

Our Effects will now only run once, so the count captured by Effects is the first render count (0), so count + 1 is always 1

One way to fix this is to use the function argument setState, setCount(count => count + 1), so that we can read the latest state, but this is not a panacea, for example, we cannot read the latest props, So if we need to setState according to the latest props, we won’t be able to do that

Use the Refs

Back to the previous problem, count cannot be read correctly because the value of count always refers to the first render.

So if we dynamically change every time we rendersetInterval(fn, delay)If the fn function has the latest props and state, and if the fn function can persist from render to render, setInterval can read the function in real time to get the latest value

The first version implements:

function setInterval(callback) {
  const savedCallback = useRef();

  useEffect((a)= > {
    savedCallback.current = callback;
  });

  useEffect((a)= > {
    const tick = (a)= > savedCallback.current();
    const id = setInterval(tick, 1000);
    return (a)= >clearInterval(id); } []); }Copy the code

Final version with support for dynamic delay and pause:

function setInterval(callback, delay) {
  const savedCallback = useRef();

  useEffect((a)= > {
    savedCallback.current = callback;
  });

  useEffect((a)= > {
    const tick = (a)= > savedCallback.current();
    if(delay ! = =undefined) {
      const id = setInterval(tick, delay);
      return (a)= > clearInterval(id);
    }
  }, [delay]);
}
Copy the code

We can use this hook to do something even more fun — to control the speed of one interval over another, as we saw in the original GIF.

practice

function Counter() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log(`Clicked ${count} times`);
    }, 3000);
  });

  return [
    <h1>{count}</h1>,
    <button onClick={() => setCount(count + 1)}>
      click me
    </button>
  ];
}
Copy the code

Guess the print?

See how the class Component behaves?

class Counter extends React.Component { state = { count: 0 }; componentDidUpdate() { setTimeout(() => { console.log(`Clicked ${this.state.count} times`); }, 3000); } render() { const { count } = this.state; return [ <h1>{count}</h1>, <button onClick={() => this.setState({ count: count + 1 })}> click me </button> ]; }}Copy the code

How to modify the above class Component to print different values like the hook component?

How can the hook version print the same values as the previous class Component?

function Counter() {
  const [count, setCount] = useState(0);
  const saved = useRef(count);

  useEffect(() => {
    saved.current = count;
    setTimeout(() => {
      console.log(`Clicked ${saved.current} times`);
    }, 3000);
  });

  return [
    <h1>{count}</h1>,
    <button onClick={() => setCount(count + 1)}>
      click me
    </button>
  ];
}
Copy the code

Other articles in the column

  • ReactiveX Streaming programming – Start with XStream
  • Abstract form to form center
  • What you don’t know about Terminal

Reference: overreacted. IO/making – seti…