React 中的 Ref
2023 react组件被调用时会新建一个实例,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>
</>
);
}