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 timer
setInterval(fn, delay)
, includingfn
callsavedCallback
. - First render, set
savedCallback
为callback1
- Second render, set
savedCallback
为callback2
- .
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!