This is the first day of my participation in Gwen Challenge

preface

The thing is, working overtime on the weekend to catch up on the project, there is a synchronous data function for asynchronous process, need to write a poll to get the synchronization result. This function is simple, I am familiar with polling ah!

A setInterval should solve the problem. So, I write the functional code without thinking, the test is too lazy to test the deployment of direct test. (This is stupid and irresponsible, don’t do it.)

The function code was written using React hooks, setInterval didn’t implement polling the way I wanted it to, and then I wondered??

Problem analysis

Due to urgent needs, I temporarily changed the code to a Class component, redistributed a version, and the problem was solved

But things can’t go on like that. I have to think, why do setInterval and hooks fail together?

Let’s implement a manual timer example to show the root cause of the failure of setInterval and clearInterval in hooks.

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

Do you think there’s something wrong with this code? Think about it for a few minutes, and then read on!

React defaults to re-executing useEffect every time it renders. When you call clearInterval and reset setInterval, the timing will be reset. If frequent re-rendering causes useEffect to be executed frequently, the timer might not fire at all! And the timer will be dead. That’s why the polling I wrote didn’t work!

To solve the problem

If you have used hooks, you know that useEffect takes a second argument, passes in a dependency array, and re-executes effect whenever the dependency array changes, not every time you render.

So if we pass an empty array [] as a dependency, so that the component is executed at mount time and cleaned up at component destruction time, wouldn’t that solve the problem?

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

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

But in reality, the timer stops when it reaches 1. Again, the timer failed to implement polling.

Why did the phenomenon not match expectations? In fact, if you look closely, you will find that this is a closure pit!

The count used by useEffect is taken during the first rendering. When you get it, it’s 0. Since effect is never re-executed, the count used in the closure by setInterval is always from the first rendering, so count + 1 is always 1. Did you suddenly understand! If you want to get a memorized count from hooks, that’s when useRef comes into play

UseRef, hooks with memory

From the above two failures, we can summarize two contradictions we found:

UseEffect has no memory. Every time it is executed, it cleans up the previous effect and sets a new effect. New effects get new props and state;

SetInterval does not forget that it will keep referring to the old props and state until it is changed. But if it’s changed, it resets the time;

I know that hooks have a memory. That is useRef.

What if, instead of replacing the timer when effect is re-executed, we pass in a memorized savedCallback variable that always points to the latest timer callback?

Our plan looks something like this:

  • Set timersetInterval(fn, delay), includingfncallsavedCallback.
  • First render, setsavedCallbackcallback1
  • Second render, setsavedCallbackcallback2
  • .

Let’s try to rewrite this using useRef:

function Counter() {
  let [count, setCount] = useState(0);
  const savedCallback = useRef();
  
  function callback() {
  	// Can read the latest state and props
  	setCount(count + 1);
	}
  
  // Every render, update ref to the latest callback
  useEffect(() = > {
    savedCallback.current = callback;
  });

  useEffect(() = > {
    let id = setInterval(() = > {
       savedCallback.current();
    }, 1000);
  	return () = > clearInterval(id); } []);return <div>{count}</div>;
}
Copy the code

On the one hand, if [] is passed in, our effect will not be re-executed, so the timer will not be reset. On the other hand, with the savedCallback ref set, we can get the callback set at the last render and call it when the timer fires. Now the data is remembered, the problem is solved, but it is too much trouble, very bad readability!

SetInterval = useInterval = setInterval = useInterval = setInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval = useInterval

useInterval

While the code above is a bit verbose, hooks have the power to extract some logic and reorganize and abstract it into a custom hooks for reuse.

I want our code to end up like this:

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

  useInterval(() = > {
    setCount(count + 1);
  }, 1000);

  return <div>{count}</div>;
}
Copy the code

So we extracted the logic and defined a hooks, which we called useInterval for better semantics

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

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

  useEffect(() = > {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, 1000);
    return () = > clearInterval(id); } []); }Copy the code

Here the delay value is written dead, we need to parameterize, considering that if the delay changes, we also need to restart the timer, so we need to put the delay in the useEffect dependency. Give it a makeover:

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

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

  useEffect(() = > {
    function tick() {
      savedCallback.current();
    }

    let id = setInterval(tick, delay);
    return () = > clearInterval(id);
  }, [delay]);
}
Copy the code

Now we don’t need to worry about this much logic, use timers in hooks, just use useInterval instead of setInterval.

But what if you want to pause the timer? Delay = null; delay = null; delay = null;

/ / the final version
function useInterval(callback,delay) {
  const savedCallback = useRef();

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

  useEffect(() = > {
    function tick() {
      savedCallback.current();
    }

   	if(delay ! = =null) {
      let id = setInterval(tick, delay);
      return () = > clearInterval(id);
  	}
  }, [delay]);
}

function Counter() {
  const [count, setCount] = useState(0);
  const [delay, setDelay] = useState(1000);
  const [isRunning, setIsRunning] = useState(true);

  useInterval(() = > {
    setCount(count + 1);
  }, isRunning ? delay : null);

  return <div>{count}</div>;
}
Copy the code

By now, our useInterval can handle all kinds of possible changes: delay value changes, pauses and continues, much more powerful than the original setInterval!

conclusion

Hooks and classes are two different programming patterns, and we may have some strange problems using Hooks, but don’t panic, we need to find the root cause of the problem and then change our thinking to fix it, not the old thinking.

Finally, thank you for reading this, I’m going to change my polling code, see you later!