React: useState or useRef?



我正在阅读有关 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不会。

它们之间的共同点是,useStateuseRef都可以在重新渲染后记住他们的数据。因此,如果您的变量是决定视图层渲染的东西,请使用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但不想触发其更改useCallbackuseEffect

我们可以通过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 之间的主要区别是 -

  1. 引用的值在组件重新渲染之间保持不变(保持不变),

  2. 使用useRef更新引用不会触发组件重新呈现。 但是,更新状态 c会导致组件重新呈现

  3. 引用更新是
  4. 同步的,更新的引用值立即可用,但状态更新是异步的 - 值在重新呈现后更新。

查看使用代码:

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

相关内容

  • 没有找到相关文章

最新更新