概述
useRef
引用一个不需要被渲染的值
const ref = useRef(initialValue)
参考
useRef(initialValue)
调用 useRef
: 在组件的最顶层声明 ref
:
import {useRef} from 'react';
function MyComponent() {
const intervalRef = useRef(0);
const inputRef = useRef(null);
}
参数
initialValue
ref
对象 current
属性初始化的值。可以是任意类型。第一次渲染之后此初始值会被忽略
返回值
useRef
返回只有一个属性的对象:
current
初始值: 设置的 initialValue
后续可以修改为其他值
如果传递了 ref
对象作为 ref
属性给 JSX
节点,React
会将其设置为 useRef
的 current
属性
下一次渲染: useRef
会返回相同的对象
附加说明
- 可以更新
ref.current
属性值。和state
不一样,useRef
是可以直接更新值的。但是如果useRef
的值需要被渲染在页面上,那么建议不用useRef
- 当
ref.current
属性更新时,React
不会再次渲染组件。React
不会意识到你改变了ref.current
属性,因为ref
是只是一个平平无奇的JS
对象 - 除了在初始化的情况下,不要在渲染(组件)时使用
ref.current
, 这会使组件的渲染行为不可预测(结果也许不尽人意) - 严格模式下,
React
为了找到多余的非纯函数会调用两次组件函数。这只会在开发环境下发生而不会影响到生产环境。这意味着每个ref
对象会被创建两次,而其中一个ref
对象会被销毁。如果组件函数是纯函数(事实上每个组件函数都应该是),这对组件逻辑不会产生任何影响
用法
用法1: 用 ref
存值
调用 useRef
在组件顶层声明一个或多个 refs
import {useRef} from 'react';
function Stopwatch() {
const intervalRef = useRef(0)
//...
}
useRef
返回只有一个 current 属性
的 ref 对象
,初始值为设定的 initial value
在下一次渲染时,useRef
会返回相同的对象。可以使用 ref
的 current
属性存储值并稍后读取
和 state
有点类似,但 ref
和 state
有着重要的区别
beta.reactjs.org/reference/r… - state
改变 ref
的值不会触发组件的重新渲染。 因此 refs
非常适合用来存储不会影响组件页面渲染的信息
eg
:如果你需要存储一个 interval ID
并稍后使用它,那么就可以将它赋值给 ref
在 ref
更新这个值,可以重新赋值给 current 属性
function handleStartClick() {
const intervalId = setInterval(() => { xxx }, 1000);
intervalRef.current = intervalId;
}
之后就可以从 ref
中读取到 interval ID
, 从而清除该定时器:
function handleStopClick() {
const inervalId = intervalRef.current;
clearInterval(intervalId);
}
用 ref
很明确的是:
- 可以在组件重新渲染之间存储信息(而常规的变量,每次渲染都会重置)
- 更改值不会触发重复渲染 (
state
变量会触发新的渲染) - 存储的信息对每个组件副本来说都是局部/本地的(不像外部的其他变量是共享的)
改变 ref
的值不会触发组件的重复渲染,因此 ref
不适合用来存储需要在页面上渲染的信息。这种情况下用 useState
更为合适
beta.reactjs.org/learn/refer… - useRef 和 useState 的使用区别
用 ref
存值 例子
1 Click counter
此组件使用 ref
来追踪按钮的点击次数。此处可以使用 ref
而不用 state
是因为点击的次数只在事件处理函数中读取
如果 JSX
中展示 {ref.current}
, 点击按钮后这个值不会更新。因为 ref.current
不会触发页面渲染。用来渲染页面的信息应该使用 state
而不是 ref
import {useRef} from 'react';
export default function Counter() {
const ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
alert(ref.current + 'times');
}
return (
<button onClick={handleClice}>Click me</button>
)
}
2 A stopwatch
此例同时用了 state
和 ref
. startTime
和 now
都是 state
变量,因为他们用于页面渲染
我们需要 intervalId
来处理按钮停止计时。因为 intervalId
没有用来渲染,所以将其存在 ref
中,人为控制更新
import {useState, useRef} from 'react';
export default function Stopwatch() {
const [startTime, setStartTime] = useState(null);
const [now, setNow] = useState(null);
const intervalRef = useRef(null);
function handleStart() {
setStartTime(Date.now());
setNow(Date.now());
clearInterval(intervalRef.current);
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
if(startTime !== null && now !== null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>Time passed: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>Start</button>
<button onClick={handleStop}>Stop</button>
</>
)
}
陷阱
在渲染中不要写入或读取 ref.current
React
希望组件像纯函数那样运行:
beta.reactjs.org/learn/keepi… - 纯函数
- 如果输入(
props
,state
和context
)一样,那组件返回值也需要返回完全一样的JSX
beta.reactjs.org/learn/passi… - props
beta.reactjs.org/learn/state… - state
beta.reactjs.org/learn/passi… - context
- 用不同的顺序或者传入不同的参数调用时不能影响其他调用的结果
在渲染过程中写入或读取 ref
不符合 React
的期待:
function MyComponent() {
//🚩 Don't write a ref during rendering
myRef.current = 123;
//🚩 Don't read a ref during rendering
return <h1>{myOtherRef.current}</h1>
}
可以在 事件处理函数 或者 effects
中写入或读取 refs
function MyComponent() {
useEffect(() => {
//✅ You can read or write refs in effects
myRef.current = 123;
});
function handleClick() {
//✅ You can read or write refs in event handlers
doSomething(myOtherRef.current);
}
}
如果必须要在渲染过程中写入和读取一些信息,可以用 state
如果打破了上述规则,也许组件也能运作,但是React
的大部分新特性都依赖于上述规则
用法2: 用 ref
操作 DOM
使用 ref
操作 DOM
元素是非常常见的。React
有内置支持
首先, 声明一个初始值为 null
的 ref
对象:
import {useRef} from 'react';
function MyComponent() {
const inputRef = useRef(null);
}
然后,将 ref
对象作为 ref
属性传递给你想操控的 DOM
节点的 JSX
:
return <input ref={inputRef}/>;
在 React
创建完 DOM
节点和将其渲染到页面之后,React
会将 ref
对象的 current
属性设置到该 DOM
节点。 然后就可以进入 <input/>
的 DOM
节点中调用 focus()
之类的方法:
function handleClick() {
inputRef.current.focus();
}
当节点从页面中移除时,React
会将 current
属性设置为 null
beta.reactjs.org/learn/manip… - 使用 refs
操作 DOM
用 ref
操作 DOM
的例子
1 Focusing a text input
点击 按钮 会自动聚焦
import {useRef} from 'react';
export default function Form() {
const inputRef = useRef(null);
function handleClick() {
inputRef.current.focus();
}
return (
<>
<input ref={inputRef}/>
<button onClick={handleClick}>Focus the input</button>
</>
)
}
2 Scrolling an image into view
点击 按钮 会将图片滑动到可视区。在 DOM
节点中设置了 list ref
,然后调用 DOM
querySelectorAll API
定位我们想要滑动的图片
import {useRef} from 'react';
export default function CatFriends() {
const listRef = useRef(null);
function scrollToIndex(index) {
const listNode = listRef.current;
// This lines assumes a particular DOM structure:
const imgNode = listNode.querySelectorAll('li > img')[index];
imgNode.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
})
}
return (
<>
<nav>
<button onClick={() => scrollToIndex(0)}>
Tom
</button>
<button onClick={() => scrollToIndex(1)}>
Maru
</button>
<button onClick={() => scrollToIndex(2)}>
Jellylorum
</button>
</nav>
<div>
<ul ref={listRef}>
<li>
<img
src="https://placekitten.com/g/200/200"
alt="Tom"
/>
</li>
<li>
<img
src="https://placekitten.com/g/300/200"
alt="Maru"
/>
</li>
<li>
<img
src="https://placekitten.com/g/250/200"
alt="Jellylorum"
/>
</li>
</ul>
</div>
</>
);
}
3 Playing and pausing a video
使用 ref
调用 <video>
DOM
节点 的 play()
和 pause()
import {useState, useRef} from 'react';
export default function VideoPlayer() {
const [isPlaying, setIsPlaying] = useState(false);
const ref = useRef(null);
function handleClick() {
const nextIsPlaying = !isPlaying;
setIsPlaying(nextIsPlaying);
if(nextIsPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}
return (
<>
<button onClick={handleClick}>{isPlaying ? 'Pause' : 'Play'}</button>
<video
width='250'
ref={ref}
onPlay={() => setIsPlaying(true)}
onPause={() => setIsPlaying(false)}
>
<source
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
type="video/mp4"
/>
</video>
</>
)
}
避免重复创建 ref
React
只会保存一次 ref
的初始值 且在下一次渲染中忽略:
function Video() {
const playerRef = useRef(new VideoPlayer());
}
尽管 new VideoPlayer()
的结果只会用于首次渲染,但在每次渲染中都会调用这个函数。这是一种浪费,因为它正在创建不必要的对象
下面这种初始化 ref
的方式可以解决上面的问题:
function Video() {
const playerRef = useRef(null);
if(playerRef.current === null) {
playerRef.current = new VideoPlayer();
}
}
通常,在渲染过程中写入或读取 ref.current
是不允许的。然而,在这种情况下是可以的,因为结果总是一样的,而且这个条件只在初始化过程中执行,所以它是完全可以预测的
如何避免 useRef
初始化后的 null
检查
如果你使用类型校验但是不想它检查 null
值,你可以尝试使用以下这种方式:
function Video() {
const playerRef = useRef(null);
function getPlayer() {
if(playerRef.current !== null) {
return playerRef.current;
}
const player = new VideoPlayer();
playerRef.current = player;
return player;
}
}
如此,playerRef
就可以为 null
。但是此时需要让类型校验判定 getPlayer()
的返回值不可能为 null
,那就可以在事件处理函数中使用 getPlayer()
疑问解答
不能在自定义组件中获取 ref
如果你像下面这样传递 ref
到组件中:
const inputRef = useRef(null);
return <MyInput ref={inputRef} />
也许你会在控制台中收到一个警告:
Warning:
Function components cannot be given refs.
Attempts to access this ref will fail.
Did you mean to use React.forwardRef() ?
默认情况下,你的组件不会暴露 refs
到组件内部的 DOM
节点
解决上述问题,可以找到你需要传递 ref
值的组件:
export default function MyInput({value, onChange}) {
return (
<input
value={value}
onChange={onChange}
/>
)
}
使用 forwardRef
包裹这个组件:
beta.reactjs.org/reference/r… - forwardRef
import {forwardRef} from 'react';
const MyInput = forwardRef(({value, onChange}, ref)=>{
return (
<input
value={value}
onChange={onChange}
ref={ref}
/>
)
})
export default MyInput;
如此,父组件就可以获取到此组件的 ref
我有话说
今天花了一天时间看 React
新官网的 useRef
的讲解,理解深入了一丢丢,本人水平有限,要是有错误,欢迎并感谢看官大佬们指出