编程知识 cdmana.com

Closure problem in react hooks

Preface

I'm picking up the lunch box at noon today , At dinner , Eating deep-sea cod fillets , Dip it in ketchup , That's delicious , It's beyond words . All of a sudden, the product rushed by and said :“ Today's demand can go online ?” I suddenly a tiger body shake , Think of a problem you have encountered, and you can't find the reason for it , Timidly replied :“ can ... Yes ...”, Products hear ‘ can ’ This word will hum a little song and go away , Leave me alone , In the face of deep-sea cod that has gone bad ... Thinking over and over again about how to solve the problem ...

One 、 from JS The closure in talking about

JS The closure of is essentially derived from two points , Lexical scope and function current value passing .

The formation of closures is very simple , After the function is executed , Return function , Or keep the function , That is to form a closure .

About Lexical scope Related knowledge , You can refer to 《 You don't know JavaScript》 Find out .

React Hooks The closure in and we are in JS There's no difference in closures seen in .

Define a factory function createIncrement(i), Return to one increment function . Every time you call increment Function time , The value of the internal counter will increase i.

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
    }
    return increment
}
const inc = createIncrement(10)
inc() // 10
inc() // 20
 Copy code 

createIncrement(10) Returns an incremental function , This function is assigned to inc Variable . When calling inc() when ,value Variable plus 10.

First call inc() return 10, The second call returns 20, And so on .

call inc() Without parameters ,JS You can still get the current value and i The incremental , See how it works .

The principle is createIncrement() in . When a function is returned on a function , There will be closures . The closure captures the variables in the lexical scope value and i.

Lexical scope is the external scope that defines a closure . In this case ,increment() The lexical scope of is createIncrement() Scope of action , It contains variables value and i.

No matter where you call inc(), Even in createIncrement() Beyond the scope of , It can all be accessed value and i.

A closure is one that can be derived from its Lexical scope Remember and modify functions of variables , No matter what the scope of execution is .

Two 、React Hooks Closure in

By simplifying state reuse and side effect management ,Hooks Instead of class based components . Besides , We can extract the custom logic to Hook in , In order to reuse between applications .Hooks Heavily dependent on JS Closure , But closures can be tricky .

When we use a React When the component , One of the problems you may encounter is the outdated closure , It can be hard to solve .

3、 ... and 、 Outdated closures

Factory function createIncrement(i) Return to one increment function .increment Function pair value increase i , And returns a record of the current value Function of

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState amount to logValue function 
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(10)
const log = inc() // 10, Change the current value Fixed value 
inc() // 20
inc() // 30

log() // "Current value is 10"  Failed to print correctly 30
 Copy code 

I'd like to mention here useRef, Why when you use let Declarative useRef You don't have this problem , While using let This problem is encountered when declaring variables , This is because useRef It's not a basic type variable , It's an object , Every time you change the value , What you're actually modifying is the value of the object , Objects are referenced by pointers , So no matter where the value is, you can get the latest value !

function createIncrement(i) {
    let value = 0
    function increment() {
        value += i
        console.log(value)
        const message = `Current value is ${value}`
        return function logValue() { // setState amount to logValue function 
            console.log(message)
        }
    }
    return increment
}
const inc = createIncrement(1) // i Fixed as 1, The input number is fixed to a number 
inc() // 1
const log = inc() // 2
inc() // 3

log() // "Current value is 2"  Failed to print correctly 3
 Copy code 

Obsolete closures capture variables with obsolete values .

Four 、 Fix the problem of obsolete closures

(1) Use a new closure

The first way to solve obsolete closures is to find closures that capture the latest variables .

Find and capture the latest message Closure of variables , From the last call inc() Closure returned .

const inc = createIncrement(1)
inc() // 1
inc() // 2
const latestLog = inc()
latestLog() // "Current value is 3"
 Copy code 

That's all React Hook How to deal with closure freshness .

Hooks The implementation assumes that before the component is re rendered , Most of all Hook The latest closure provided by the callback ( for example useEffect(callback)) The latest variables have been captured from the functional scope of the component . That is to say useEffect Second parameter of [] Add the value of monitoring changes , At every change , perform function, Get the latest closure .

(2) Close the changed variable

The second way is to make logValue() Use it directly value.

Let's move the line const message = ...; To logValue() Function body :

function createIncrementFixed(i) {
  let value = 0;
  function increment() {
    value += i;
    console.log(value);
    return function logValue() {
      const message = `Current value is ${value}`;
      console.log(message);
    };
  }
  
  return increment;
}

const inc = createIncrementFixed(1);
const log = inc(); //  Print  1
inc();             //  Print  2
inc();             //  Print  3
//  Normal work 
log();             //  Print  "Current value is 3"
 Copy code 

logValue() close createIncrementFixed() In scope value Variable .log() Now print the correct message .

5、 ... and 、Hook Obsolete closures in

useEffect()

In the use of useEffect Hook The common situation of the closure appears when .

In components WatchCount in ,useEffect Print per second count Value .

function WatchCount() {
    const [count, setCount] = useState(0)
    useEffect(function() {
        setInterval(function log() {
            console.log(`Count is: ${count}`)
        }, 2000)
    }, [])
    
    return (
      <div>
      {count}
      <button onClick={() => setCount(count + 1)}>  Add 1 </button>
      </div>
    )
}
 Copy code 

Click a few times to add 1 Button , We see from the console , Every time 2 Seconds printed as Count is: 0

In the first rendering ,log() Closure capture in count The value of the variable 0. later , Even if count increase ,log() The initialization value is still used in 0.log() The closure in is an obsolete closure .

resolvent : Give Way useEffect() know log() Closures in depend on count:

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

  useEffect(function() {
    const id = setInterval(function log() {
      console.log(`Count is: ${count}`);
    }, 2000);
    return function() {
      clearInterval(id);
    }
  }, [count]); //  Look here , This line is the point ,count Re render after change useEffect

  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1) }>
        Increase
      </button>
    </div>
  );
}
 Copy code 

After setting the dependency , once count change ,useEffect() Just update the closure .

Properly manage Hook Dependency is the key to solving the problem of obsolete closures . Recommended installation eslint-plugin-react-hooks, It can help us detect forgotten dependencies .

useState()

Components DelayedCount Yes 2 Button

  • Click the key “Increase async” In asynchronous mode with 1 Second delay up counter
  • In synchronous mode , Click the key “Increase sync” Will immediately increase the counter
function DelayedCount() {
  const [count, setCount] = useState(0);

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count + 1);
    }, 1000);
  }

  function handleClickSync() {
    
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  )
}
 Copy code 

Click on “Increase async” Press the button and immediately click “Increase sync” Button ,count Update only to 1.

This is because delay() It's an outdated closure .

Let's see what happens in this process :

Initial rendering :count The value is 0. Click on 'Increase async' Button .delay() Closure capture count Value 0.setTimeout() 1 Seconds later delay(). Click on “Increase async” Key .handleClickSync() call setCount(0 + 1) take count Is set to 1, Component re render . 1 Seconds later ,setTimeout() perform delay() function . however delay() In the closure save count The value of is the value of the initial rendering 0, So call setState(0 + 1), result count Keep for 1.

delay() It's an outdated closure , It uses outdated... Captured during initial rendering count Variable .

To solve this problem , You can use functional methods to update count state :

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

  function handleClickAsync() {
    setTimeout(function delay() {
      setCount(count => count + 1); //  This line is the point 
    }, 1000);
  }

  function handleClickSync() {
    setCount(count + 1);
  }

  return (
    <div>
      {count}
      <button onClick={handleClickAsync}>Increase async</button>
      <button onClick={handleClickSync}>Increase sync</button>
    </div>
  );
}
 Copy code 

Now? setCount(count => count + 1) Updated delay() Medium count state .React Ensure that the latest state value is provided as a parameter to the update state function , The problem with outdated closures is solved .

useLayoutEffect()

useLayoutEffect It can be seen as useEffect Synchronization version of .

useLayoutEffect Its function signature and useEffect identical , But it will be in all DOM Synchronous call after change effect. You can use it to read DOM Layout and sync trigger re rendering . Before the browser does the drawing ,useLayoutEffect The internal update plan will be refreshed synchronously .

therefore , Use standard as much as possible useEffect To avoid blocking visual updates . because useLayoutEffect It's synchronous , If we want to useLayoutEffect Call state update , Or perform some very time-consuming calculations , May lead to React Too long running time , Blocking browser rendering , Leading to some problems with Caton .

If you're moving code from class Component migration to use Hook The function component of , You need to pay attention to useLayoutEffect And componentDidMountcomponentDidUpdate The call phase of is the same . however , We recommend that you start with useEffect, Only try to use it when it goes wrong useLayoutEffect.

If you use server-side rendering , please remember , No matter what useLayoutEffect still useEffect Can't be in Javascript Execute before code loading is complete . This is why the introduction of useLayoutEffect Code will trigger React The alarm . Solve this problem , You need to move the code logic to useEffect in ( If you don't need this logic for the first rendering ), Or delay the component until the client rendering is finished ( If until useLayoutEffect Perform before HTML All show confusion ).

To render from the server HTML Exclude dependency layout from effect The components of , By using showChild && <Child /> Do conditional rendering , And use useEffect(() => { setShowChild(true); }, []) Delay presentation components . such , Before the client rendering is complete ,UI It's not going to be as disorganized as before .

summary

Closure is a function , It starts from where the variable is defined ( Or its lexical scope ) Capture variables .

When a closure captures Obsolete variables when , There's the problem of outdated closures .

An effective method to solve closure

  1. Set correctly React Hook The dependencies of
  2. For the out of date state , Update state using function mode

author :FruitBro
link :https://juejin.cn/post/684790...
source : Nuggets
The copyright belongs to the author . Commercial reprint please contact the author for authorization , Non-commercial reprint please indicate the source .

版权声明
本文为[Alfred]所创,转载请带上原文链接,感谢
https://cdmana.com/2020/12/20201224100315732a.html

Scroll to Top