2

This article is the third in a series of in-depth ahoos source code articles, which have been organized into document- address . I think it's not bad, give a star to support it, thanks.

This article explores how ahooks solves React's closure problem? .

React's closure problem

Let's look at an example first:

 import React, { useState, useEffect } from "react";

export default () => {
  const [count, setCount] = useState(0);

  useEffect(() => {
    setInterval(() => {
      console.log("setInterval:", count);
    }, 1000);
  }, []);

  return (
    <div>
      count: {count}
      <br />
      <button onClick={() => setCount((val) => val + 1)}>增加 1</button>
    </div>
  );
};

code example

When I click the button, I find that the value printed in setInterval has not changed, it is always 0. This is React's closure problem.

Causes

In order to maintain the state of the Function Component, React uses a linked list to store the hooks in the Function Component, and creates an object for each hook.

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

The memoizedState property of this object is used to store the last updated state , next pointing to the next hook object. In the process of component update, the order of execution of hooks functions is unchanged, and you can get the Hook object corresponding to the current hooks according to this linked list. This is how functional components have the ability to state .

At the same time, a series of rules are formulated, for example, hooks cannot be written to if...else... . This ensures that the state of the corresponding hook can be obtained correctly.

useEffect takes two parameters, a callback function and an array. The array is the dependency of useEffect. When it is [], the callback function will only be executed once when the component is rendered for the first time . If there are dependencies on other items, react will determine whether the dependencies have changed, and if so, the callback function will be executed.

Back to the example just now:

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

useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, []);

When it is executed for the first time, useState is executed, and count is 0. Execute useEffect, execute the logic in its callback, start the timer, and output setInterval: 0 every 1s.

When I click the button to increase count by 1, the entire functional component is re-rendered, and the linked list of the previous execution already exists at this time. useState sets the state saved on the Hook object to 1, then count is also 1 at this time. But when useEffect is executed, its dependencies are empty and the callback function is not executed. But the previous callback function is still there, it will still be executed every 1s console.log("setInterval:", count); , but the count here is the count value when it was executed for the first time, because it is referenced in the callback function of the timer , forming the closure has been preserved.

the solution

Solution 1: Set dependencies for useEffect, re-execute the function, set a new timer, and get the latest value.

 // 解决方法一
useEffect(() => {
  if (timer.current) {
    clearInterval(timer.current);
  }
  timer.current = setInterval(() => {
    console.log("setInterval:", count);
  }, 1000);
}, [count]);

Solution 2: Use useRef.
useRef returns a mutable ref object whose .current property is initialized to the incoming parameter (initialValue).

useRef creates a normal Javascript object, and returns the same ref object every time it is rendered . When we change its current property, the reference to the object is the same, so the timer can read the latest value .

 const lastCount = useRef(count);

// 解决方法二
useEffect(() => {
  setInterval(() => {
    console.log("setInterval:", lastCount.current);
  }, 1000);
}, []);

return (
  <div>
    count: {count}
    <br />
    <button
      onClick={() => {
        setCount((val) => val + 1);
        // +1
        lastCount.current += 1;
      }}
    >
      增加 1
    </button>
  </div>
);

useRef => useLatest

Finally back to our ahoos theme, based on the second solution above, the useLatest hook was born. It returns the Hook of the current latest value, which avoids the closure problem. The implementation principle is very simple, there are only ten lines of code, that is, use the useRef package:

 import { useRef } from 'react';
// 通过 useRef,保持每次获取到的都是最新的值
function useLatest<T>(value: T) {
  const ref = useRef(value);
  ref.current = value;

  return ref;
}

export default useLatest;

useEvent => useMemoizedFn

Another scenario in React is based on useCallback.

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

const callbackFn = useCallback(() => {
  console.log(`Current count is ${count}`);
}, []);

No matter how much the value of our count changes, the value of count printed out by executing callbackFn is always 0. This is because the callback function is cached by useCallback to form a closure, thus forming a closure trap.

So how do we solve this problem? Officially proposed useEvent. The problem it solves: how to keep function references unchanged and access the latest state at the same time. After using it, the above example becomes.

 const callbackFn = useEvent(() => {
  console.log(`Current count is ${count}`);
});

We don't take a closer look at this feature here. In fact, a similar function has been implemented in ahooks, that is, useMemoizedFn.

useMemoizedFn is a Hook for the persistence function. In theory, useMemoizedFn can be used to completely replace useCallback. With useMemoizedFn, the second parameter deps can be omitted while guaranteeing that the function address will never change. The above problems can be easily solved by:

 const memoizedFn = useMemoizedFn(() => {
  console.log(`Current count is ${count}`);
});

Demo address

Let's take a look at its source code, and we can see that it still keeps the function reference address unchanged through useRef, and can get the latest state value every time it is executed.

 function useMemoizedFn<T extends noop>(fn: T) {
  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const fnRef = useRef<T>(fn);
  fnRef.current = useMemo(() => fn, [fn]);
  // 通过 useRef 保持其引用地址不变,并且值能够保持值最新
  const memoizedFn = useRef<PickFunction<T>>();
  if (!memoizedFn.current) {
    // 返回的持久化函数,调用该函数的时候,调用原始的函数
    memoizedFn.current = function (this, ...args) {
      return fnRef.current.apply(this, args);
    };
  }

  return memoizedFn.current as T;
}

Summary and thinking

Since the introduction of hooks in React, although some drawbacks of class components have been solved, for example, logic reuse needs to be nested layer by layer through higher-order components. But it also introduces some problems, such as the closure problem.

This is caused by React's Function Component State management, which sometimes confuses developers. Developers can avoid it by adding dependencies or using useRef.

ahooks is also aware of this problem and avoids similar closure traps through useLatest to ensure the latest value and useMemoizedFn to persist the function.

It is worth mentioning that useMemoizedFn is the standard of ahooks output function, and all output functions use useMemoizedFn to wrap one layer. In addition, the input functions are recorded once using useRef to ensure that the latest functions can be accessed anywhere.

refer to


Gopal
366 声望77 粉丝