组件被调用时会新建一个实例,ref 可以指向该实例,如果是原生 DOM 组件,我们可以得到 DOM 节点。

用 Refs 引用值

当你想一个组件记住一些信息,但你不想这些信息触发新的渲染,你就可以使用 ref。

使用

使用 useRef 可以添加一个新的 ref。

import { useRef } from 'react';

function App() {
  const ref = useRef(0);
  //...
}

useRef 会返回一个对象, 使用 ref.current 可以获取当前的值。

{
  current: 0; // 使用 useRef 传递的值
}

State 区别

下面是 Ref 与 State 的区别对比

Ref State
useRef(initialValue) 返回 {current: initialValue} useState(initialValue) 返回当前值和设置函数 [value, setValue]
修改时候不会触发重新渲染 修改的时候触发重新渲染
可变的 - 可以直接修改更新值 不可变的 - 必须使用设置函数修改值等待重新渲染
在渲染的时候不要读取/修改 current 任何时候都可以读取 state 值

一直强调修改 ref 不会触发渲染,以下是来自官方的一个例子 codesandbox

import { useRef } from 'react';

export default function Counter() {
  let countRef = useRef(0);

  function handleClick() {
    // This doesn't re-render the component!
    countRef.current = countRef.current + 1;
  }

  return <button onClick={handleClick}>You clicked {countRef.current} times</button>;
}

内部实现

在 React 内部,可以想象 useRef 的实现如下:

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

使用

当我们需要“走出” React 组件并与外部 API 通信时,可能会使用到 Ref。

  • 保存 timeout IDs
  • 保存并操纵 DOM 元素
  • 保存不需要 JSX 计算的对象

引用 DOM

Ref 可以引用值,但是我们更多是的使用是引用 DOM 元素。

有时候我们需要访问 React 下面的 DOM 元素,如给 input 聚焦/失焦,滚动,或者测量位置,我们就需要使用 ref 引用到对应的 DOM 元素。

import { useRef } from 'react';

function App() {
  const myRef = useRef(null);

  return <div ref={myRef}></div>;
}

这样子我们就可以通过事件处理访问到 DOM 元素内置的浏览器 API,如:myRef.current.scrollIntoView();

其他

forwardRef

当我们直接使用 Ref 给子组件的时候,经常会收到一个错误提示。以下是来自官方的例子 codesandbox

import { useRef } from 'react';

function MyInput(props) {
  return <input {...props} />;
}

export default function MyForm() {
  const inputRef = useRef(null);

  function handleClick() {
    inputRef.current.focus();
  }

  return (
    <>
      <MyInput ref={inputRef} />
      <button onClick={handleClick}>Focus the input</button>
    </>
  );
}

在打印里我们看到如下的错误:

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

发生这种情况是因为,默认情况下,React 不允许组件访问另一个组件的 DOM 元素。如果需要暴露其 DOM 元素,就需要使用到 forwardRef API:

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

ref 列表

在列表中遍历循环声明 ref 并不会起作用,如:

<ul>
  {items.map((item) => {
    // Doesn't work!
    const ref = useRef(null);
    return <li ref={ref} />;
  })}
</ul>

但是可以通过 ref 使用 Map,在 Map 里面保存每个节点的相关信息。

export default function CatFriends() {
  const itemsRef = useRef(null);

  function scrollToId(itemId) {
    const map = getMap();
    const node = map.get(itemId);
    node.scrollIntoView({
      behavior: 'smooth',
      block: 'nearest',
      inline: 'center',
    });
  }

  function getMap() {
    if (!itemsRef.current) {
      // Initialize the Map on first usage.
      itemsRef.current = new Map();
    }
    return itemsRef.current;
  }

  return (
    <>
      <div>
        <ul>
          {catList.map((cat) => (
            <li
              key={cat.id}
              ref={(node) => {
                const map = getMap();
                if (node) {
                  map.set(cat.id, node);
                } else {
                  map.delete(cat.id);
                }
              }}
            >
              <img src={cat.imageUrl} alt={'Cat #' + cat.id} />
            </li>
          ))}
        </ul>
      </div>
    </>
  );
}

参考链接