Today's problem is also rooted in life (work).When we first started using React hook s, we often encountered situations where I needed to update the value of my state in an asynchronous request/event listener and do something with the updated state.It is possible to encounter this situation at this time, the value of state has not been updated, we always get the old state.Why is this the case?What can we do to solve it?This article will take you through these issues.
1. Problem recurrence
First come demo As follows:
import React from "react"; import "./styles.css"; const { useState, useEffect } = React; export default function App() { const [scrollCount, setScrolledCount] = useState(0); useEffect(() => { console.log(`useEffect: ${scrollCount}`); }); const handleScroll = () => { console.log(`handleScroll: ${scrollCount}`); setScrolledCount(scrollCount + 1); }; useEffect(() => { document.addEventListener("scroll", handleScroll); return () => document.removeEventListener("scroll", handleScroll); // ⚠️Note: This is an empty array, which means only run once when the component is mounted }, []); return ( <div className="App"> <h1>React hook Get the latest state Demo</h1> </div> ); }
In this example, we listen for scroll events and continue to add scrollCount to 1 while continuing to output the value of scrollCount.Undoubtedly, our expected results should be:
useEffect: 0 handleScroll: 0 useEffect: 1 handleScroll: 1 useEffect: 2 handleScroll: 2 ...
But the actual result is:
useEffect: 0 handleScroll: 0 useEffect: 1 handleScroll: 0 // Components are not re-rendered and useEffect is not running because the value of state has not been updated handleScroll: 0 handleScroll: 0 ...
So why can't we get the latest state s?
Because a closure is created when useEffect executes, the scrollCount value at run time is saved in the closure with an initial value of 0, so when scrolling, 0 is output each time
_We can refer to the React hook principle as follows: React Hook Principle "- This is the clearest I think I've ever said, none of them.
How can I solve this problem?
2.Four Solutions
2.1 useEffect removes dependent empty arrays
This solution is the simplest, with the empty array removed.This way, each useEffect runs, although still a closure, and each time the latest scrollCount value is taken, so the output of handleScroll is continuously updated.
Some students may ask: Is it okay if events are repeatedly bound and unbound?
No problem, since useEffect is called after the browser has finished laying out and drawing, and the performance overhead of binding and unbinding events is small, this is not a problem.
⚠️Note: Remember to unbind events, there must be a problem with continuously adding event bindings.
useEffect(() => { document.addEventListener("scroll", handleScroll); return () => document.removeEventListener("scroll", handleScroll); // ⚠️Changed here: Removed the second argument of useEffect, empty array });
2.2 Move functions into useEffect and add dependent States
Because useEffect relies on scrollCount, each scrollCount change causes useEffect to rerun, obtaining the latest scrollCount.
Of course, it's important to emphasize that it doesn't really matter if a function isn't moved inside the useEffect, but once it's moved, it's easier to see which state the function depends on.
useEffect(() => { // ⚠️Changed here: handleScroll was moved inside useEffect const handleScroll = () => { console.log(`handleScroll: ${scrollCount}`); setScrolledCount(scrollCount + 1); }; document.addEventListener("scroll", handleScroll); return () => document.removeEventListener("scroll", handleScroll); // ⚠️Changed here: empty array becomes [scrollCount] }, [scrollCount]);
2.3 Functional updates using setState
Functional update: If the new state needs to be calculated using the previous state, then the function can be passed to setState.This function receives the previous state and returns an updated value.Come from React Official Documentation
const handleScroll = () => { console.log(`handleScroll: ${scrollCount}`); // ⚠️Changed here: the expression becomes a function setScrolledCount(scrollCount => scrollCount + 1); };
By using this feature, we can ensure that the state is updated every time, but the scrollCount value obtained from handleScroll is still 0 in the closure.The actual output we get is as follows:
useEffect: 0 handleScroll: 0 useEffect: 1 handleScroll: 0 useEffect: 2 handleScroll: 0 ...
2.4 Use global variables
Looking back at this issue, it is simply that a state is not up-to-date, and we can solve this problem by using global variables, either by mounting the state under window s or by using useRef.
This change code changes slightly, pasting it all below:
import React from "react"; import "./styles.css"; const { useRef, useEffect } = React; export default function App() { const scrollCountRef = useRef(null); useEffect(() => { console.log(`useEffect: ${scrollCountRef.current}`); }); const handleScroll = () => { console.log(`handleScroll: ${scrollCountRef.current}`); scrollCountRef.current++; }; useEffect(() => { scrollCountRef.current = 0; document.addEventListener("scroll", handleScroll); return () => document.removeEventListener("scroll", handleScroll); }, []); return ( <div className="App"> <h1>React hook Get the latest state Demo</h1> </div> ); }
Of course, this code does not involve component re-rendering, so its output is:
useEffect: null handleScroll: 1 handleScroll: 2 handleScroll: 3 ...
3. Summary
In this article, we discuss the causes of the problem and four ways to get the latest state s for React Hook.The problem itself is not complex, but a thorough understanding of its principles and a continuous summary are the source of our continuous progress.