ref 常见的用法是拿到输入框、调用 focus。它还可以保存一个跨渲染存在的值,但它和 state 的用法不同:修改 ref.current 不会让页面重新渲染。
异步回调里很容易看出这个区别。点下按钮时 count 是 0,延迟回调执行前又点了一次加一,回调读到的 count 会是什么?
创建一个空目录,安装 React 和打包工具。示例不用 JSX,省去 Babel 配置,组件逻辑和 JSX 的写法是一样的。
npm init -y
npm install --save-exact react@18.3.1 react-dom@18.3.1
npm install --save-dev --save-exact webpack@5.99.9
创建 build.cjs:
const webpack = require("webpack");
const path = require("path");
webpack(
{
mode: "development",
entry: path.resolve("main.js"),
output: { path: path.resolve("dist"), filename: "main.js" },
devtool: "source-map",
},
(error, stats) => {
if (error || stats.hasErrors()) {
console.error(error || stats.toString("errors-only"));
process.exitCode = 1;
}
},
);
创建 index.html:
<!doctype html>
<html lang="zh-CN">
<meta charset="utf-8" /><meta name="viewport" content="width=device-width" />
<title>React 实验</title>
<div id="root"></div>
<script src="./dist/main.js"></script>
</html>
把下面的代码存为 main.js,运行 node build.cjs,然后用浏览器打开 index.html。这里用开发构建和根节点 StrictMode,观察次数时要把这两个条件算进去。
import React, {
useState,
useRef,
useEffect,
useMemo,
useContext,
memo,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
const e = React.createElement;
window.lab = { effects: [], contexts: { stable: 0, unstable: 0 }, measurements: [] };
function Refs() {
const [count, setCount] = useState(0);
const latest = useRef(count);
const input = useRef(null);
const [log, setLog] = useState("");
useEffect(() => {
latest.current = count;
}, [count]);
return e(
"section",
null,
e("h2", null, "ref 与异步回调"),
e("input", { ref: input, "aria-label": "ref input" }),
e("button", { id: "focus", onClick: () => input.current.focus() }, "聚焦"),
e("button", { id: "increment", onClick: () => setCount((v) => v + 1) }, "加一"),
e(
"button",
{
id: "delayed",
onClick: () =>
setTimeout(
() => setLog("snapshot=" + count + ", latest=" + latest.current),
200,
),
},
"延迟读取",
),
e("p", { id: "count" }, count),
e("p", { id: "ref-log" }, log),
);
}
createRoot(document.querySelector("#root")).render(e(StrictMode, null, e(Refs)));
点聚焦,焦点移到输入框。点延迟读取,随后在 200ms 内点加一,页面会显示 snapshot=0, latest=1。
count 来自创建回调的那次渲染,所以保留了当时的值。latest 是稳定的 ref 对象,读取 latest.current 时拿到已经同步进去的新值。这两个结果都合理,关键是回调需要哪一种语义。
如果是在提交订单,点击时确认的金额可能就应该保留快照;如果是持续运行的订阅回调,可能需要最新配置。不能看到“旧值”就一律换成 ref。
ref 也有同步时机
这里在 effect 中更新 latest.current,表示提交以后再同步。它不是任意时刻都和 state 完全一致;刚触发更新、提交尚未完成时,ref 可能还没有更新。
示例的延迟任务只是显示一行结果。如果换成请求或长期定时器,要补上取消和卸载清理。需要页面随着值改变而更新的内容,仍然放 state。把所有状态改成 ref,只会让界面不知道什么时候更新。
还要避免在渲染过程中随意写 ref.current。渲染可能被重复执行或放弃,渲染期间的修改不适合承担外部同步。使用规则参考 React useRef 文档。