useRef 是一个 React Hook,它能让你引用一个不需要渲染的值。
点击计时器
useRef 返回的值在函数组件中不会自动触发重新渲染,所以控制台可以显示变化而按钮上无法显示 ref.current的变化。
import { useRef } from "react";
const ClinkNumber = () => {
let ref = useRef(0);
function handleClick() {
ref.current = ref.current + 1;
// 每次触发函数会跟随变动 +1
console.log(ref.current);
}
return (
<div>
// ref.current 不会跟随变动
<button onClick={handleClick}>点击计时器 {ref.current}</button>
</div>
);
};
export default ClinkNumber;
解决这个问题的方法是使用 React 的状态管理来保存并更新计数器的值。
const [counter, setCounter] = useState(0);
秒表
需要将 interval ID 保存在 ref 中,以便在需要时能够清除计时器。
import { useRef, useState } from "react";
const SecondWatch = () => {
const [startTime, setStartTime] = useState<any>(null);
const [now, setNow] = useState<any>(null);
const intervalRef = useRef<any>(null);
function handleStart() {
// Date.now() 用于获取当前时间的时间戳。它返回的是一个表示当前时间的整数值,以毫秒为单位。
setStartTime(Date.now());
setNow(Date.now());
// 清除上一个定时器
clearInterval(intervalRef.current);
// 定时器
intervalRef.current = setInterval(() => {
setNow(Date.now());
}, 10);
}
function handleStop() {
// 关闭定时器
clearInterval(intervalRef.current);
}
let secondsPassed = 0;
// 毫秒数除以 1000,以将其转换为秒。
if (startTime != null && now != null) {
secondsPassed = (now - startTime) / 1000;
}
return (
<>
<h1>秒表计时: {secondsPassed.toFixed(3)}</h1>
<button onClick={handleStart}>开始</button>
<button onClick={handleStop}>暂停</button>
</>
);
};
export default SecondWatch;
使用了 useRef 创建了一个名为 intervalRef 的引用对象,初始值为 null。当点击 “Start” 按钮时,我清除了之前可能存在的计时器(如果有),并创建了一个新的计时器。计时器的 ID 被保存在 intervalRef.current 中。
当点击 “Stop” 按钮时,我清除了当前的计时器,并将 intervalRef.current 重置为 null。
这样,我们通过 intervalRef.current 来保存和更新计时器的 ID,在需要时可以清除计时器,同时避免了将计时器显示在界面上。
聚焦文字输入框
当用户点击按钮时,handleClick 函数会被调用,从而将焦点聚焦到文本输入框上。
// 焦文字输入框
import { useRef } from "react";
const FocusForm = () => {
const inputRef = useRef<any>(null);
function handleClick() {
// 获取输入框焦点
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>Focus the input</button>
</>
);
};
export default FocusForm;
定义一个名为 handleClick 的函数。当按钮被点击时,这个函数会被调用。在函数内部,我们通过 inputRef.current 获取到 inputRef 引用所指向的 DOM 元素,并调用其 focus 方法,将焦点聚焦到文本输入框上。
滚动图片到视图
定义了一个名为 scrollToIndex 的函数,它接受一个参数 index。当按钮被点击时,这个函数会被调用,并根据传入的 index 值来滚动到对应的图片。
import { useRef } from "react";
const RollPicture = () => {
const listRef = useRef(null);
function scrollToIndex(index: any) {
const listNode: any = listRef.current;
// 这一行假设了一个特定的 DOM 结构
const imgNode = listNode.querySelectorAll("li > img")[index];
imgNode.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "center",
});
}
return (
<>
<nav>
<button onClick={() => scrollToIndex(0)}>猫 - 0 </button>
<button onClick={() => scrollToIndex(1)}>猫 - 1 </button>
<button onClick={() => scrollToIndex(2)}>猫 - 2 </button>
</nav>
<div>
<ul ref={listRef}>
<li>
<span>猫 - 0 </span>
<img src="https://placekitten.com/g/200/200" alt="Tom" />
</li>
<li>
<span>猫 - 1 </span>
<img src="https://placekitten.com/g/300/200" alt="Maru" />
</li>
<li>
<span>猫 - 2 </span>
<img src="https://placekitten.com/g/250/200" alt="Jellylorum" />
</li>
</ul>
</div>
</>
);
};
export default RollPicture;
在函数内部,我们首先通过 listRef.current 获取到 listRef 引用所指向的 DOM 元素,这里是一个 ul 元素。
然后,我们通过 listNode.querySelectorAll("li > img")[index] 获取到特定索引位置的图片元素 imgNode。这里假设了一个特定的 DOM 结构,即 ul 元素下的每个 li 元素中都包含一个 img 元素。
最后,我们调用 imgNode.scrollIntoView() 方法,将图片滚动到视图中。我们通过传入一个配置对象来指定滚动的行为,包括 behavior(滚动行为)、block(垂直方向对齐方式)和 inline(水平方向对齐方式)。
在组件的返回值中,我们渲染了一个包含按钮和图片列表的结构。
在按钮部分,我们为每个按钮设置了一个点击事件,并通过调用 scrollToIndex 函数来滚动到对应的图片。每个按钮都有一个不同的索引值作为参数。
在图片列表部分,我们将 listRef 设置为 ul 元素的 ref 属性,以便将引用与该元素关联起来。同时,我们渲染了几个 li 元素,每个元素包含一个图片和一个描述。
这样,当用户点击按钮时,对应的图片会滚动到视图中心。
拓展一下:imgNode.scrollIntoView()
imgNode.scrollIntoView()是一个 DOM 方法,用于将指定的元素滚动到可见区域。在这个代码片段中,我们使用了一个配置对象作为
scrollIntoView()方法的参数。配置对象包括以下属性:
behavior:指定滚动的行为,这里设置为"smooth",表示平滑滚动。block:指定垂直方向上的对齐方式,这里设置为"nearest",表示滚动到最近的边界。inline:指定水平方向上的对齐方式,这里设置为"center",表示水平居中对齐。通过使用这些配置,
scrollIntoView()方法会将imgNode元素滚动到可见区域,并以平滑的方式进行滚动,使其在垂直和水平方向上都居中对齐。
开始或暂停视频
当用户点击按钮时,根据当前视频的状态,我们会开始或暂停视频,并根据视频的播放状态来更新按钮的显示文本。
// 播放及暂停视频
import { useRef, useState } from "react";
const ControlsVideo = () => {
const [isPlaying, setIsPlaying] = useState<any>(false);
const ref = useRef<any>(null);
function handleClick() {
// 按钮名的修改
const nextIsPlaying = !isPlaying;
// 开始或暂停视频
setIsPlaying(nextIsPlaying);
if (nextIsPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}
return (
<>
<button onClick={handleClick}>{isPlaying ? "开始" : "暂停"}</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>
</>
);
};
export default ControlsVideo;
我们定义了一个函数组件 ControlsVideo。在组件内部,我们使用 useState 创建了一个名为 isPlaying 的状态变量,并将初始值设置为 false,表示视频初始状态是暂停的。我们还使用 useRef 创建了一个引用 ref,并将其初始化为 null。
接下来,我们定义了一个叫做 handleClick 的函数。当按钮被点击时,这个函数会被调用。
在函数内部,我们首先通过取反操作符 ! 来获取下一个状态 nextIsPlaying。然后,我们调用状态更新函数 setIsPlaying,将 nextIsPlaying 设置为新的状态值,从而更新 isPlaying 的值。
接着,我们使用 ref.current 来获取到 ref 引用所指向的 DOM 元素,这里是一个 video 元素。
根据 nextIsPlaying 的值,如果视频需要播放
-
调用
ref.current.play()方法来播放视频; -
如果视频需要暂停,我们调用
ref.current.pause()方法来暂停视频。
在组件的返回值中,我们渲染了一个按钮和一个视频元素。
在按钮部分,我们通过调用 handleClick 函数来处理按钮的点击事件。根据 isPlaying 的状态,我们使用条件运算符 ? 和 : 来显示不同的按钮名,如果 isPlaying 为 true,显示 “开始”,否则显示 “暂停”。
在视频元素部分,我们将 ref 设置为 video 元素的 ref 属性,以便将引用与该元素关联起来。同时,我们通过 onPlay 和 onPause 事件处理函数来更新 isPlaying 的状态,当视频开始播放时将其设置为 true,当视频暂停时将其设置为 false。并且,我们指定了视频源的 URL 和类型。
向你的组件暴露 ref
要暴露 ref 最关键的就是 forwardRef
forwardRef是 React 中的一个高阶函数,用于在函数组件中将 ref 属性向下传递给子组件。在 React 中,我们可以使用
ref属性来获取对一个组件实例的引用,以便在父组件中操作子组件。但是,当我们在一个函数组件中使用ref属性时,默认情况下,React 不会将该ref属性传递给函数组件内部的 DOM 元素或其他组件。这就是
forwardRef函数的作用。它接受一个回调函数作为参数,该回调函数接收两个参数:props和ref。在回调函数中,我们可以将ref属性传递给子组件的特定元素或组件。通过使用
forwardRef函数,我们可以将父组件传递给函数组件的ref属性转发给子组件的 DOM 元素或其他组件。这样,我们就能够在父组件中通过使用函数组件的引用操作子组件。
案例:
自定义 React 组件暴露引用 (ref) , 将 inputRef 引用传递给了 MyInput 组件,并且在父组件中通过操作引用来控制子组件内部的行为。
// 向你的组件暴露 ref
import { forwardRef, useRef } from "react";
// 暴露了 MyInput 函数
const MyInput = forwardRef((props, ref: any) => {
return <input {...props} ref={ref} />;
});
function ExposeRef() {
// 获取到 MyInput 的 ref
const inputRef = useRef<any>(null);
function handleClick() {
// 操作 MyInput 的 ref
inputRef.current.focus();
}
return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>获取焦点</button>
</>
);
}
export default ExposeRef;
定义了一个函数组件 MyInput,通过使用 forwardRef 包裹组件,以便能够将父组件传递的 ref 属性传递给子组件。forwardRef 函数接受一个回调函数作为参数,该回调函数接收两个参数:props 和 ref。在这个回调函数中,我们使用 input 元素来展示输入框,并使用展开操作符 ...props 将父组件传递给 MyInput 的其他属性应用到 input 元素上,同时将 ref 设置为传递进来的 ref。
接着,我们定义了一个名为 ExposeRef 的函数组件。在这个组件内部,我们使用 useRef 创建了一个名为 inputRef 的引用。
在组件的返回值中,我们使用 MyInput 组件,并将 inputRef 作为 ref 属性传递给它。这样,inputRef 就可以在 MyInput 组件内部访问到 input 元素的引用。
同时,我们渲染了一个按钮,并在点击事件的处理函数中操作了 inputRef 的引用。在 handleClick 函数中,我们调用 inputRef.current.focus() 来聚焦到 input 元素上。这样,当用户点击按钮时,输入框会获取焦点。