First, this article doesn’t cover the basic use of hooks, it tries to explain cause and effect by starting with a “weird” (and logical) “closure trap” scenario in hooks. In the meantime, useRef is in many react hooks articles, so why is it possible to get around the closure trap by using useRef? I think getting these issues straight would greatly improve my understanding of React hooks.

Many developers have been following react hooks since they were first introduced, and perhaps every developer has encountered “closure traps” when using React hooks, some of which are blindsided, others which define the problem instantly.

React 16.8.3 React 16.8.3

You must have encountered the following scenario:

function App(){
    const [count, setCount] = useState(1);
    useEffect((a)= >{
        setInterval((a)= >{
            console.log(count)
        }, 1000)}, [])}Copy the code

If you print the value of count in this timer, it will print 1 no matter how many times you setCount to any value elsewhere in the component using setCount. Is there a feeling that, despite thousands of sails, I still remember you as you were? hhh… Next, I will try to clarify my understanding of why this is happening and talk about some of the other features of hooks. If there are mistakes, I hope you can save the children, do not let me live with the wrong cognition…

1. A familiar closure scenario

Let’s start with a scenario that all of you Jser are familiar with.

for ( var i=0; i<5; i++ ) {
    setTimeout((a)= >{
        console.log(i)
    }, 0)}Copy the code

The year I graduated, this question was a popular interview question. But now…

I’m not going to talk about why you end up printing all 5’s. Print 0 directly using closures… Code for 4:

for ( var i=0; i<5; i++ ) {
   (function(i){
         setTimeout((a)= >{
            console.log(i)
        }, 0)
   })(i)
}
Copy the code

The idea is to use closures. The timer’s callback refers to a variable defined in the immediate function. The closure holds the value of I at the time the immediate function is executed, and the asynchronous timer’s callback prints the sequential values we want.

The useEffect closure trap scenario is a natural consequence of the react component update process and the implementation of the useEffect.

2. Briefly discuss hooks principle and understand the cause of useEffect “closure trap”.

In fact, I really don’t want to involve the React principle in writing this article, because it is really too comprehensive (in fact, the main reason is the dishes, and I just swallowed them without understanding them). You need to understand the general process and some important points that support the general process.

First, you may have heard of react’s Fiber architecture. A Fiber node is a component. While it’s normal for classComponents to have state, there is a memoizedState on the Fiber object that holds the component’s state. Ok, now look at FunctionComponnet targeted by hooks. No matter what developers do, an object can only have one state property or memoizedState property, but who knows how many useState, useEffect, etc lovely developers will put in FunctionComponent? So react uses a linked list as a data structure to store hooks in FunctionComponent. Such as:

function App(){
    const [count, setCount] = useState(1)
    const [name, setName] = useState('chechengyi')
    useEffect((a)= >{}, [])const text = useMemo((a)= >{
        return 'ddd'}}, [])Copy the code

When the component is first rendered, an object is created for each hooks

type Hook = {
  memoizedState: any,
  baseState: any,
  baseUpdate: Update<any.any> | null,
  queue: UpdateQueue<any.any> | null,
  next: Hook | null};Copy the code

The result is a linked list.

The memoizedState property of this object is used to store the state of the component since its last update, and next is no doubt pointing to the next hook object. During component updates, the hooks function executes in the same order, so that the hooks corresponding to the current hooks can be retrieved from the list. At the moment, the implementation is much more complicated than that.

So, know why you can’t write hooks to if else statements? This can result in an out-of-order order in which the current hooks do not get their own Hook object.

UseEffect takes two arguments, a callback function and an array. Inside the array is the useEffect dependency. When [], the callback is executed only once when the component is first rendered. React determines if a dependency has changed and executes a callback if it has. Back to the original scene:

function App(){
    const [count, setCount] = useState(1);
    useEffect((a)= >{
        setInterval((a)= >{
            console.log(count)
        }, 1000)}, [])function click(){ setCount(2)}}Copy the code

Well, start imagining that the component first renders App() and useState sets the initial state to 1, so count is 1. UseEffect is then executed, the callback is executed, and a timer is set to print count every 1s.

If the click function is triggered, a call to setCount(2) will trigger the react update, which will also execute App() when updating the current component. So now count is going to be 2. UseEffect is not executed because the dependency array is an empty array.

Ok, this update did not involve the timer at all, the timer still persisted, silently, printing count every 1s. Note that the count printed here is the count of App() when the component was first rendered. Count has a value of 1 because it is referenced in the timer callback function, creating a closure that is kept forever.

Does 2 really need to write values in the dependency array to get fresh values?

We tend to think that the only way to get the freshest values in the update process is to write the values we need in the dependency array. So look at this scenario:

function App() {
  return <Demo1 />
}

function Demo1(){
  const [num1, setNum1] = useState(1)
  const [num2, setNum2] = useState(10)

  const text = useMemo(()=>{
    return `num1: ${num1} | num2:${num2}`
  }, [num2])

  function handClick(){
    setNum1(2)
    setNum2(20)
  }

  return (
    <div>
      {text}
      <div><button onClick={handClick}>click!</button></div>
    </div>)}Copy the code

Text is a useMemo with only num2 in its dependent array and no NUM1 in it, but uses both states. When the button is clicked, the values of num1 and num2 are changed. Can we get the freshest value of num1 in a text that only depends on num2?

If you have react esLint installed, you might get an error here because you used num1 in text but didn’t add it to the dependency array. However, executing this code will show that the freshest num1 value is normally available.

If you understand the “closure trap” problem mentioned in point 1, you will understand this problem as well.

React relies on num2 to determine whether the react callback needs to be executed during the update. Num2 has changed. The callback function will execute naturally, and the resulting closure will refer to the latest NUM1 and num2, so it will get fresh values. The key is when the callback is executed. The closure is like a camera that stores the values of the callback at that point in time. I think the callback function of the timer mentioned before is just like a person who has traversed to the modern age from 1000 years ago. Although he has come to the modern age, his blood and hair are from 1000 years ago.

Why is it possible to get fresh values every time using useRef?

Baymax: Because when the initial useRef is executed, it returns the same object. Write here the baby can not help but recall just learn JS that time, holding a little red book to chew the scene:

var A = {name: 'chechengyi'}
var B = A
B.name = 'baobao'
console.log(A.name) // baobao
Copy the code

Yeah, that’s what makes this scene work.

That is, every time a component is rendered. For example, if ref = useRef() returns the same object and the ref generated each time the component is updated points to the same memory space, then of course you can get the freshest value each time. Has Inuyasha seen it? An ancient well connects the modern world to the Warring States period 500 years ago, and the same object also links variables that were stored at different closure times.

Here’s an example that might make sense:

    /* Write these related variables outside the function to mimic the react hooks object */
	let isC = false
	let isInit = true; // Simulate the component loading for the first time
	let ref = {
		current: null
	}

	function useEffect(cb){
		// This is used to simulate useEffect only once when the dependency is [].
 		if (isC) return
		isC = true	
		cb()	
	}

	function useRef(value){
		// Set the value if the component is first loaded otherwise return the object directly
		if ( isInit ) {
			ref.current = value
			isInit = false
		}
		return ref
	}

	function App(){
		let ref_ = useRef(1)
		ref_.current++
		useEffect((a)= >{
			setInterval((a)= >{
				console.log(ref.current) / / 3
			}, 2000)})}// Perform the first component load and the second component update twice in a row
	App()
	App()
Copy the code

So, come up with a reasonable scenario. As long as we can ensure that useState returns the same object every time the component is updated? Can we also bypass the closure trap scenario? Give it a try.

function App() {
  // return <Demo1 />
  return <Demo2 />
}

function Demo2(){
  const [obj, setObj] = useState({name: 'chechengyi'})

  useEffect(()=>{
    setInterval(()=>{
      console.log(obj)
    }, 2000)
  }, [])
  
  function handClick(){
    setObj((prevState)=> {
      var nowObj = Object.assign(prevState, {
        name: 'baobao',
        age: 24
      })
      console.log(nowObj == prevState)
      return nowObj
    })
  }
  return (
    <div>
      <div>
        <span>name: {obj.name} | age: {obj.age}</span>
        <div><button onClick={handClick}>click!</button></div>
      </div>
    </div>)}Copy the code

Just to give you a sense of this code, when you do setObj, you pass in a function. I don’t need to tell you how to use it, do I? Then object. assign returns the first Object passed in. In short, the same object is returned when set.

After the button is clicked, the value printed by the timer also changes to:

{
    name: 'baobao'.age: 24 
}
Copy the code

4 to complete

React hooks are react-hooks. After writing this article, baby, I know hooks better than EVER. I hope it helps others who have had the same problem before. If it will help you, promise me, please don’t be stingy with your likes.