Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

useRef #169

Open
yaofly2012 opened this issue Sep 21, 2020 · 1 comment
Open

useRef #169

yaofly2012 opened this issue Sep 21, 2020 · 1 comment

Comments

@yaofly2012
Copy link
Owner

yaofly2012 commented Sep 21, 2020

image

useRef

一、动机

  1. 函数组件访问DOM元素;
  2. 函数组件访问之前渲染变量。
    函数组件每次渲染都会被执行,函数内部的局部变量一般会重新创建,利用useRef可以访问上次渲染的变量,类似类组件的实例变量效果。

1.2 函数组件使用React.createRef不行吗?

可以,但不是最佳实践
React.createRef主要解决class组件访问DOM元素问题,并且最佳实践是在组件周期内只创建一次(一般在构造函数里创建)。如果在函数组件内使用React.createRef会造成每次render都会调用React.createRef

function WithCreateRef() {
  const [minus, setMinus] = useState(0);
  // 每次render都会重新创建`ref`
  const ref = React.createRef(null);

  const handleClick = () => {
    setMinus(minus + 1);
  };

  // 这里每次都是`null`
  console.log(`ref.current=${ref.current}`)

  useEffect(() => {
    console.log(`denp[minus]>`, ref.current && ref.current.innerText);
  }, [minus]);

  return (
    <div className="App">
      <h1 ref={ref}>Num: {minus}</h1>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

二、使用

2.1 基本语法

见文档

  1. 每次渲染useRef返回值都不变;
  2. ref.current发生变化并不会造成re-render;
  3. ref.current发生变化应该作为Side Effect(因为它会影响下次渲染),所以不应该在render阶段更新current属性。

2.2 不可以render里更新ref.current

Is there something like instance variables提到:

Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.

render里更新refs导致什么问题呢?
在异步渲染里render阶段可能会多次执行。

const RenderCounter = () => {
  const counter = useRef(0);
  
  // counter.current的值可能增加不止一次
  counter.current = counter.current + 1;
  
  return (
    <h1>{`The component has been re-rendered ${counter.current} times`}</h1>
  );
}

2.3 可以render里更新ref.current

同样也是在Is there something like instance variables提到的:

Unless you’re doing lazy initialization, avoid setting refs during rendering — this can lead to surprising behavior. Instead, typically you want to modify refs in event handlers and effects.

为啥lazy initialization却可以在render里更新ref.current值?
这个跟useRef懒初始化的实现方案有关。

const instance = React.useRef(null)
if (instance.current == null) {
  instance.current = {
    // whatever you need
  }
}

本质上只要保证每次render不会造成意外效果,都可以在render阶段更新ref.current。但最好别这样,容易造成问题,useRef懒初始化毕竟是个特殊的例外

2.4 ref.current 不可以作为其他hooks(useMemo, useCallback, useEffect)依赖项

ref.current的值发生变更并不会造成re-render, Reactjs并不会跟踪ref.current的变化。

function Minus() {
  const [minus, setMinus] = useState(0);
  const ref = useRef(null);

  const handleClick = () => {
    setMinus(minus + 1);
  };

  console.log(`ref.current=${ref.current && ref.current.innerText}`)

  // #1 uesEffect
  useEffect(() => {
    console.log(`denp[ref.current] >`, ref.current && ref.current.innerText);
  }, [ref.current]);

  // #2 uesEffect
  useEffect(() => {
    console.log(`denp[minus]>`, ref.current && ref.current.innerText);
  }, [minus]);

  return (
    <div className="App">
      <h1 ref={ref}>Num: {minus}</h1>
      <button onClick={handleClick}>Add</button>
    </div>
  );
}

本例子中当点击[Add]两次后#1 uesEffect就不会再执行了。
image

原因分析:
依赖项判断是在render阶段判断的,发生在在ref.current更新之前,而useEffect的effect函数执行在渲染之后。

  1. 第一次执行:
    首次无脑执行,所以输出:
ref.current=null
denp[ref.current] > Num: 0
denp[minus]> Num: 0

并且此时ref.currentnull,所以 #1 uesEffect相当于useEffect(() => console.log('num 1'), [null])

  1. 点击[Add],第二次执行:
    此时ref.current值为<h1>Num: 0<h1>,所以 #1 uesEffect的依赖项发生变化,最终输出:
ref.current=Num: 0
denp[ref.current] > Num: 1
denp[minus]> Num: 1

此时 #1 uesEffect相当于useEffect(() => console.log('num 1'), [<h1>Num: 0<h1>])

  1. 点击[Add],第三次执行:
    此时ref.current值为<h1>Num: 1<h1>,所以 #1 uesEffect的依赖项没有发生变化,故 #1 uesEffect的effect函数不会被执行,最终输出:
ref.current=Num: 1
denp[minus]> Num: 2

如果将ref.current作为依赖项,eslint-plugin-react-hooks也会报警提示的:

React Hook useEffect has an unnecessary dependency: 'ref.current'. Either exclude it or remove the dependency array. Mutable values like 'ref.current' aren't valid dependencies because mutating them doesn't re-render the component react-hooks/exhaustive-deps

2.5 ref作为其他hooks(useMemo, useCallback, useEffect)依赖项

ref是不变的,没必要作为其他hooks依赖。但是添加也无妨,有时候为了更明确表达依赖项也吧ref写入依赖数组里。

三、什么时候使用?

  1. 访问DOM
  2. 访问之前渲染函数里的变量(类似类成员变量)

四、原理

image
本质上是记忆hook,但也可作为data hook,可以简单的用useState模拟useRef

const useRef = (initialValue) => {
  const [ref] = useState({ current: initialValue});
  return ref
}

4.1 如何选择useStateuseRef

useStateuseRef都可以作为data hook,那什么时候使用useState,什么时候使用useRef呢?

原则:

  1. 如果当变量的值发生变化时想重新渲染组件,则选择state;
  2. 如果只是为了缓存变量值(即下次重新渲染时获取上次的变量值),则选择ref。

4.2 在生命周期哪个阶段处理?

image

参考

  1. Is there something like instance variables?
  2. Demystifying React Hooks: useRef
  3. Meduim: React useRef Hook
  4. React Memorized Hook — useCallback & useMemo ( React.memo )
  5. Lazy useRef instance variables
  6. useEffect will unpredictable when depends on ref (useRef)
@yaofly2012 yaofly2012 changed the title useRef ? useRef Sep 21, 2020
@yaofly2012
Copy link
Owner Author

useRef应用场景

1. 引用组件的实例,元素

这个是最基本的用法

2. 实现“成员变量”

2.1 useEventCallback

背景:

  1. 一直以来useCallback的使用姿势都不对

2.2 useMounted

import { useRef, useEffect } from 'react';

function useIsMountedRef(): { readonly current: boolean } {
  const isMountedRef = useRef(false);

  useEffect(() => {
    isMountedRef.current = true;

    return () => {
      isMountedRef.current = false;
    };
  }, []);

  return isMountedRef;
}

export default useIsMountedRef;

背景:

  1. Avoid Memory Leak With React SetState On An Unmounted Component
  2. isMounted is an Antipattern

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant