# React 中的 Ref

5 min read
Table of Contents

组件被调用时会新建一个实例,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 的区别对比

RefState
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>
</>
)
}

参考链接


More Posts