我正在阅读有关 ReactuseState()
的信息,并在"Hooks FAQ"上useRef()
,我对一些似乎同时具有 useRef 和 useState 解决方案的用例感到困惑,我不确定哪种方式是正确的。
来自关于 useRef() 的 "Hooks FAQ":
"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);
};
});
// ...
}
两个例子都会有相同的结果,但哪一个更好 - 为什么?
两者之间的主要区别是:
useState
会导致重新渲染,useRef
不会。
它们之间的共同点是,useState
和useRef
都可以在重新渲染后记住他们的数据。因此,如果您的变量是决定视图层渲染的东西,请使用useState
.否则使用useRef
我建议阅读这篇文章。
当您想要跟踪值更改但又不想触发重新渲染或useEffect
时,useRef
很有用。
大多数用例是当您有一个依赖于值的函数,但该值需要由函数结果本身更新时。
例如,假设您要对某些 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
的帮助下从 deps 数组中删除currentPage
依赖项useCallback
因此我们的组件从无限循环中保存下来。
useState 和 useRef 之间的主要区别是 -
-
引用的值在组件重新渲染之间保持不变(保持不变),
-
使用
useRef
更新引用不会触发组件重新呈现。 但是,更新状态 c会导致组件重新呈现
引用更新是 同步的,更新的引用值立即可用,但状态更新是异步的 - 值在重新呈现后更新。
查看使用代码:
import { useState } from 'react';
function LogButtonClicks() {
const [count, setCount] = useState(0);
const handle = () => {
const updatedCount = count + 1;
console.log(`Clicked ${updatedCount} times`);
setCount(updatedCount);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
每次单击该按钮时,它都会显示我渲染了!
但是,随着useRef
import { useRef } from 'react';
function LogButtonClicks() {
const countRef = useRef(0);
const handle = () => {
countRef.current++;
console.log(`Clicked ${countRef.current} times`);
};
console.log('I rendered!');
return <button onClick={handle}>Click me</button>;
}
我被渲染的控制台只会记录一次。
基本上,我们在那些情况下使用UseState,在这种情况下,状态的值应该通过重新渲染来更新。
当您希望您的信息在组件的生命周期内持续存在时,您将使用UseRef,因为它不适用于重新渲染。
- 计数器应用程序以查看
useRef
不会重新渲染
如果使用 useRef 创建简单的计数器应用来存储状态:
import { useRef } from "react";
const App = () => {
const count = useRef(0);
return (
<div>
<h2>count: {count.current}</h2>
<button
onClick={() => {
count.current = count.current + 1;
console.log(count.current);
}}
>
increase count
</button>
</div>
);
};
如果单击该按钮,<h2>count: {count.current}</h2>
此值将不会更改,因为组件未重新渲染。如果您检查控制台console.log(count.current)
,您将看到该值实际上在增加,但由于组件没有重新渲染,因此 UI 不会更新。
如果使用useState
设置状态,单击该按钮将重新渲染组件,以便更新 UI。
- 防止在键入
input
时进行不必要的重新渲染。
重新渲染是一项成本高昂的操作。在某些情况下,您不希望继续重新呈现应用。例如,当您将输入值存储在创建受控组件的状态中时。在这种情况下,对于每次击键,您将重新呈现应用。如果使用ref
获取对 DOM 元素的引用,则使用useState
将仅重新呈现组件一次:
import { useState, useRef } from "react";
const App = () => {
const [value, setValue] = useState("");
const valueRef = useRef();
const handleClick = () => {
console.log(valueRef);
setValue(valueRef.current.value);
};
return (
<div>
<h4>Input Value: {value}</h4>
<input ref={valueRef} />
<button onClick={handleClick}>click</button>
</div>
);
};
- 防止
useEffect
内部无限循环
要创建一个简单的翻转动画,我们需要 2 个状态值。 一个是在区间内翻转或不翻转的布尔值,另一个是在我们离开组件时清除订阅:
const [isFlipping, setIsFlipping] = useState(false);
let flipInterval = useRef<ReturnType<typeof setInterval>>();
useEffect(() => {
startAnimation();
return () => flipInterval.current && clearInterval(flipInterval.current);
}, []);
const startAnimation = () => {
flipInterval.current = setInterval(() => {
setIsFlipping((prevFlipping) => !prevFlipping);
}, 10000);
};
setInterval
返回一个 id,我们将其传递给clearInterval
以便在我们离开组件时结束订阅。flipInterval.current
为 null 或此 ID。如果我们在这里不使用ref
,每次我们从 null 切换到 id 或从 id 切换到 null 时,这个组件都会重新渲染,这将创建一个无限循环。
- 如果不需要更新 UI,请使用
useRef
存储状态变量。
假设在 react 本机应用程序中,我们为某些对 UI 没有影响的操作设置声音。对于一个状态变量,它可能没有那么多的性能节省,但是如果您玩游戏并且需要根据游戏状态设置不同的声音。
const popSoundRef = useRef<Audio.Sound | null>(null);
const pop2SoundRef = useRef<Audio.Sound | null>(null);
const winSoundRef = useRef<Audio.Sound | null>(null);
const lossSoundRef = useRef<Audio.Sound | null>(null);
const drawSoundRef = useRef<Audio.Sound | null>(null);
如果我使用useState
,每次更改状态值时都会不断重新渲染。
假设您需要为组件使用 ID。 如果您使用
useState
创建它,它将随着每次重新渲染而更改。const [id,setId]=useState(uuid.v4())
如果希望 id 不会随着每次重新渲染而更改
const id = useRef(uuid.v4());
如果存储间隔 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]);
// ...
}
你也可以使用useRef
来引用 dom 元素(默认的 HTML 属性)
例如:分配一个按钮以专注于输入字段。
而useState
仅更新值并重新呈现组件。
这实际上主要取决于您使用计时器的目的,这并不清楚,因为您没有显示组件呈现的内容。
-
如果要在组件的呈现中显示计时器的值,则需要使用 useState。否则,引用值的更改不会导致重新渲染,计时器也不会在屏幕上更新。
-
如果必须发生其他事情,这应该在计时器的每个时钟周期直观地更改 UI,您可以使用 useState 并将计时器变量放在 useEffect 钩子的依赖项数组中(您可以在其中执行 UI 更新所需的任何操作),或者根据计时器值在呈现方法(组件返回值)中执行逻辑。 SetState 调用将导致重新渲染,然后调用 useEffect 钩子(取决于依赖项数组)。 使用 ref,不会发生更新,也不会调用 useEffect。
-
如果您只想在内部使用计时器,则可以改用 useRef。每当必须发生导致重新渲染的事情时(即在经过一定时间之后),您就可以从 setInterval 回调中使用 setState 调用另一个状态变量。这将导致组件重新渲染。
只有在真正必要时(即在流或性能问题的情况下)才应该使用本地状态的 refs,因为它不遵循"React 方式"。
useRef() 只更新值而不重新渲染你的 UI 如果你想重新渲染 UI,那么你必须使用 useState() 而不是 useRe。
正如在许多不同地方所指出的useState
更新会触发组件的渲染,而useRef
更新不会。
在大多数情况下,有一些指导原则会有所帮助:
对于useState
- 与
input
/TextInput
一起使用的任何内容都应具有使用您正在设置的值更新的状态。 - 当您需要触发器来重新计算
useMemo
中的值或使用useEffect
触发效果时 - 当您需要仅在对
useEffect
或其他事件处理程序执行async
操作后可用的渲染将使用的数据时。 例如FlatList
需要提供的数据。
对于useRef
- 使用它们来存储对用户(如事件订阅者)不可见的数据。
- 对于上下文或自定义钩子,使用它来传递由
useMemo
更新的 prop 或由useState
/useReducer
触发的useEffect
。 我倾向于犯的错误是将类似authState
的东西作为状态放置,然后当我更新时,当该状态实际上是链的最终结果时,它会触发整个重新渲染。 - 当您需要通过
ref
时
简单地说,如果你只需要读取一个值并且从不更新该值,那么使用 Refs