useState & useRef

组件内状态

useState

useState 接收一个初始值,返回一个数组,数组里面分别是当前值和修改这个值的方法(类似 state 和 setState)。useState 接收一个函数,返回一个数组。setCount 可以接收新值,也可以接收一个返回新值的函数。

const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(() => 0);
setCount1(1); // 修改 state

函数式状态的粒度会比类中状态更细,函数式状态保存的是快照,类状态保存的是最新值。引用类型的情况下,类状态不需要传入新的引用,而函数式状态必须保证是个新的引用。

快照(闭包)与最新值(引用)

在函数式组件中,我们有时候会发现所谓闭包冻结的现象,譬如在如下代码中:

function App() {
  const [count, setCount] = useState(0);
  const inc = React.useCallback(() => {
    setTimeout(() => {
      setCount(count + 1);
    });
  }, []);

  return <h1 onClick={inc}>{count}</h1>;
}

类组件里面可以通过 this.state 引用到 count,所以每次 setTimeout 的时候都能通过引用拿到上一次的最新 count,所以点击多少次最后就加了多少。在函数式组件里面每次更新都是重新执行当前函数,也就是说 setTimeout 里面读取到的 count 是通过闭包获取的,而这个 count 实际上只是初始值,并不是上次执行完成后的最新值,所以最后只加了 1 次。

想要解决这个问题,那就涉及到另一个新的 Hook 方法 useRef。useRef 是一个对象,它拥有一个 current 属性,并且不管函数组件执行多少次,而 useRef 返回的对象永远都是原来那一个。

export default function App() {
  const [count, setCount] = React.useState(0);
  const ref = useRef(0);

  const inc = React.useCallback(() => {
    setTimeout(() => {
      setCount((ref.current += 1));
    });
  }, []);

  return (
    <h1 onClick={inc}>
      {count},{ref.current}
    </h1>
  );
}

useRef

export function useRef<T>(initialValue: T): { current: T } {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber();
  workInProgressHook = createWorkInProgressHook();
  let ref;

  if (workInProgressHook.memoizedState === null) {
    ref = { current: initialValue };
    // ...
    workInProgressHook.memoizedState = ref;
  } else {
    ref = workInProgressHook.memoizedState;
  }
  return ref;
}

对于函数式组件,如果我们需要获取该组件子元素的 Ref,可以使用 forwardRef 来进行 Ref 转发:

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
上一页
下一页