The problem code

Take a look at some code for closure problems caused by useEffect

const btn = useRef(); const [v, setV] = useState(''); useEffect(() => { let clickHandle = () => { console.log('v:', v); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('click', clickHandle) } }, []); const inputHandle = e => { setV(e.target.value) } return ( <> <input value={v} onChange={inputHandle} /> <button Ref ={BTN} > </button> </>)Copy the code

The useEffect dependency array is empty, so the internal code is executed only once after page rendering is complete, and page destruction once more. At this point in the input box input any character, then click the test button, the output is empty, after any character input, then click the test button, the output is still empty.

Why is that? That’s what closures do.

The reasons causing

The scope of a function is determined when the function is defined

When registering click events with BTN, the scope is as follows:

The accessible free variable v is still null. When the click event is triggered, the click callback function is executed. The execution context is created first, and the scope chain is copied to the execution context.

  • If no character is entered in the input box, click to get itvThe same as beforev
  • Called if a character was entered in the input fieldsetVTo modify thestatePage triggerrenderThe component’s internal code is reexecuted, redeclaring onev.vIt won’t be the samevHere, click in scope of the eventvOr the oldvThese are two different onesv

Generate scenarios

  • Event binding. For example, in the sample code, only one event is bound after the initial rendering of the page, such as usingechartsIn theuseEffectTo deriveechartsAnd bind events
  • The timer. The same closure problem occurs when a page is loaded and a timer is registered for the function inside the timer.

simulating

    function Component(i, v) {
        // Each component rendering redeclares a state to reallocate memory
        // The memory occupied by the previous timer is not released, so the dependent state is not released
        // So be sure to clear timers and event bindings at component destruction time to avoid memory leaks
        var state = v || 1;
        var init = i === void 0 ? false : i;
        
        if (init) {
            console.log('new state:', state);
            // Ensure that useEffect is executed only once, emulating null dependent useEffect
            return;
        }

        useEffect();

        function useEffect() {
            setInterval(function() {
            console.log('state:', state)
            }, 1000)}}// Simulate component rendering
    Component()

    setTimeout(function() {
        // After one second setState triggers the component to render, updating the value of state
        Component(true.2)},1000);
Copy the code
  1. Component rendering,stateA value of1In theuseEffectRegister timer, outputstateThe value of the
  2. Void “called” after 1 secondsetStateMake the component firere-renderAnd updatestateValue of 2
  3. Due to null dependentuseEffectExecute it once. Check it out herestateValue directly afterreturn
  4. The timer still prints 1, called for the first time because of the closureComponentThe allocated memory has not been freedre-renderAfter, reallocate the memory, now there are twostateAll allocated memory, while the timer getsstateFirst from the beginning to the endstate

The solution

There are four possible solutions to this closure problem

1. Directly modify the value in the assignment modevAnd will be modifiedvMethod of useuseCallbackwrapped

Wrap the method of modifying V with useCallback, and the function wrapped by useCallback will be cached. Since the array of dependencies is empty, the v modified by direct assignment is the old V, and this method is not recommended, because setState is the official recommended way to modify state. SetV is still used here just to trigger rerender

Var [v, setV] = useState("); var [v, setV] = useState("); const inputHandle = useCallback(e => { let { value } = e.target v = value setV(value) }, [])Copy the code
2. TouseEffectPlus the dependency ofv

This is probably the first way that most people think of. Since V is old, it is not enough to re-register an event every time V is updated. However, this will result in re-registering every TIME V is updated.

3. AvoidvBe redeclared

Declare a variable as let or var instead of v, modify the variable directly, rather than requiring the setState function to trigger render, so it will not be redeclared, and click on the callback to get the “latest” value, but this method is not recommended. For this example, The input component displays a null value all the way through because there is no rerender, not as expected.

Use 4.useRefInstead ofuseState
const btn = useRef(); const vRef = useRef(''); const [v, setV] = useStat(''); useEffect(() => { let clickHandle = () => { console.log('v:', vRef.current); } btn.current.addEventListener('click', clickHandle) return () => { btn.removeEventListener('click', clickHandle) } }, []); const inputHandle = e => { let { value } = e.target vRef.current = value setV(value) } return ( <> <input value={v} OnChange ={inputHandle} /> <button ref={BTN} > </button> </>)Copy the code

useRefIt works because every timeinputthechangeChange isvRefOf this objectcurrentProperties, andvRefAlways the samevRef, even ifrerenderBecause ofvRefIs an object, so the value of a variable stored in stack memory is the address of the object in heap memory. It is only a reference, and only a property of the object is modified, and the reference does not change. So the scope chain in the click event always accesses the same thingvRef

The code address

Click here to see the test code

If any of the above mistakes, welcome to correct.