反应:useState 还是 useRef?
Posted
技术标签:
【中文标题】反应:useState 还是 useRef?【英文标题】:React: useState or useRef? 【发布时间】:2019-10-20 16:37:12 【问题描述】:我正在“Hooks FAQ”阅读有关 React useState()
和 useRef()
的信息,我对一些似乎同时具有 useRef 和 useState 解决方案的用例感到困惑,但我不是确定哪种方式是正确的。
来自“Hooks 常见问题解答”about useRef():
“useRef() Hook 不仅仅用于 DOM refs。“ref”对象是一个通用容器,其当前属性是可变的并且可以保存任何值,类似于类上的实例属性。”
使用 useRef():
function Timer()
const intervalRef = useRef();
useEffect(() =>
const id = setInterval(() =>
// ...
);
intervalRef.current = id;
return () =>
clearInterval(intervalRef.current);
;
);
// ...
使用 useState():
function Timer()
const [intervalId, setIntervalId] = useState(null);
useEffect(() =>
const id = setInterval(() =>
// ...
);
setIntervalId(id);
return () =>
clearInterval(intervalId);
;
);
// ...
两个示例的结果相同,但哪个更好 - 为什么?
【问题讨论】:
【参考方案1】:两者的主要区别是:
useState
会导致重新渲染,useRef
不会。
它们的共同点是,useState
和useRef
在重新渲染后都可以记住它们的数据。因此,如果您的变量决定了视图层渲染,请使用useState
。否则使用useRef
我建议阅读此article。
【讨论】:
另一个很大的区别是设置状态是异步的,设置引用是同步的。 在实践中引入一个单独的(令人困惑的)钩子真的会产生如此大的不同吗? React 做很多事情都违背了最佳性能(注册和注册),但我们应该注意避免重新渲染吗?怎么会? 当您将 useRef 与原生 html 输入元素一起使用时,您的组件是不受控制的输入,而使用 useState 它是受控的【参考方案2】:基本上,我们在这些情况下使用 UseState,在这些情况下,状态的值应该通过重新渲染来更新。
当您希望您的信息在组件的整个生命周期内保持不变时,您将使用 UseRef,因为它不适用于重新渲染。
【讨论】:
【参考方案3】:如果你存储了区间 id,你唯一能做的就是结束区间。更好的是存储状态timerActive
,以便您可以在需要时停止/启动计时器。
function Timer()
const [timerActive, setTimerActive] = useState(true);
useEffect(() =>
if (!timerActive) return;
const id = setInterval(() =>
// ...
);
return () =>
clearInterval(intervalId);
;
, [timerActive]);
// ...
如果您希望回调在每次渲染时都发生变化,您可以使用 ref 在每次渲染时更新内部回调。
function Timer()
const [timerActive, setTimerActive] = useState(true);
const callbackRef = useRef();
useEffect(() =>
callbackRef.current = () =>
// Will always be up to date
;
);
useEffect(() =>
if (!timerActive) return;
const id = setInterval(() =>
callbackRef.current()
);
return () =>
clearInterval(intervalId);
;
, [timerActive]);
// ...
【讨论】:
【参考方案4】:useRef
在您想跟踪值更改但不想触发重新渲染或useEffect
时很有用。
大多数用例是当你有一个依赖于 value 的函数,但 value 需要由函数结果本身更新。
例如,假设您要对某些 API 结果进行分页:
const [filter, setFilter] = useState();
const [rows, setRows] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const fetchData = useCallback(async () =>
const nextPage = currentPage + 1;
const response = await fetchApi(...filter, page: nextPage);
setRows(response.data);
if (response.data.length)
setCurrentPage(nextPage);
, [filter, currentPage]);
fetchData
正在使用currentPage
状态,但响应成功后需要更新currentPage
。这是不可避免的过程,但它很容易在 React 中导致无限循环,也就是 Maximum update depth exceeded error
。例如,如果您想在加载组件时获取行,您需要执行以下操作:
useEffect(() =>
fetchData();
, [fetchData]);
这是有问题的,因为我们使用状态并在同一个函数中更新它。
我们想跟踪currentPage
,但不想通过其更改触发useCallback
或useEffect
。
我们可以通过useRef
轻松解决这个问题:
const currentPageRef = useRef(0);
const fetchData = useCallback(async () =>
const nextPage = currentPageRef.current + 1;
const response = await fetchApi(...filter, page: nextPage);
setRows(response.data);
if (response.data.length)
currentPageRef.current = nextPage;
, [filter]);
我们可以在useRef
的帮助下从useCallback
deps 数组中移除currentPage
依赖,这样我们的组件就免于死循环了。
【讨论】:
【参考方案5】:你也可以使用useRef
来引用一个dom元素(默认的HTML属性)
例如:分配一个按钮来关注输入字段。
而useState
只更新值并重新渲染组件。
【讨论】:
【参考方案6】:这实际上主要取决于您使用计时器的目的,这不清楚,因为您没有显示组件呈现的内容。
如果你想在组件的渲染中显示你的计时器的值,你需要使用useState。否则,您的 ref 值的变化不会导致重新渲染,并且计时器不会在屏幕上更新。
如果必须在计时器的每个滴答声处更改 UI 视觉上发生其他事情,您可以使用 useState 并将计时器变量放入 a 的依赖项数组中useEffect 挂钩(您可以在其中执行 UI 更新所需的任何操作),或者根据计时器值在渲染方法(组件返回值)中执行您的逻辑。 SetState 调用将导致重新渲染,然后调用您的 useEffect 挂钩(取决于依赖项数组)。 有了 ref,就不会发生更新,也不会调用 useEffect。
如果您只想在内部使用计时器,您可以改用 useRef。每当必须发生导致重新渲染的事情时(即经过一定时间后),您就可以在 setInterval 回调中使用 setState 调用另一个状态变量。这将导致组件重新渲染。
仅在真正需要时(即,在流量或性能问题的情况下)使用本地状态的引用,因为它不遵循“the React way”。
【讨论】:
以上是关于反应:useState 还是 useRef?的主要内容,如果未能解决你的问题,请参考以下文章