反应: useState 还是 useRef?

我在“ 钩子常见问题解答”上读到了 React useState()useRef(),我对一些似乎同时具有 useRef 和 useState 解决方案的用例感到困惑,我不确定哪种方式是正确的。

摘自“钩子常见问题解答”关于 useRef ():

”useRef () Hook 不仅仅适用于 DOM 参考。“ 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);
};
});


// ...
}

两个例子都有相同的结果,但是哪一个更好——为什么?

51473 次浏览

两者的主要区别在于:

useState会导致重新渲染,而 useRef不会。

它们之间的共同点是,useStateuseRef在重新渲染后都能记住它们的数据。因此,如果您的变量是决定视图层呈现的东西,使用 useState。否则使用 useRef

我建议读读这本 文章

基本上,在这些情况下我们使用 用户国,在这种情况下,状态的值应该通过重新呈现来更新。

当您希望您的信息在组件的生命周期内保持时,您将使用 UseRef,因为它不适合于重新呈现的工作。

如果存储间隔 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]);


// ...
}

当您想要跟踪值的变化,但不想触发 re- 呈现或 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。这是一个不可避免的过程,但在反应过程中容易产生无限循环,即 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的帮助下,我们可以从 useCallback deps 数组中移除 currentPage依赖,这样我们的组件就可以从无限循环中保存下来。

您还可以使用 useRef来引用 dom 元素(默认的 HTML 属性)

指定一个按钮将焦点集中在输入字段上。

useState只更新值并重新呈现组件。

它实际上主要取决于您使用计时器的目的,这一点并不清楚,因为您没有显示组件呈现的内容。

  • 如果希望在呈现组件时使用 显示计时器的价值,则需要使用 useState。否则,ref 值的变化不会导致重新呈现,计时器也不会在屏幕上更新。

  • 如果必须发生其他事情,而且 更改用户界面计时器的每个滴答声上可视化,那么您可以使用 useState 并将计时器变量放在 useEffect 钩子的依赖数组中(在这里您可以执行 UI 更新所需的任何操作) ,或者根据计时器值在呈现方法(组件返回值)中执行逻辑。 SetState 调用将导致重新呈现,然后调用 useEffect 挂钩(取决于依赖项数组)。 使用 ref 时,不会发生任何更新,也不会调用 useEffect。

  • 如果只想使用 在内部使用计时器,则可以使用 useRef。无论何时必须发生的事情应该导致重新渲染(即。过了一段时间后) ,您可以在 setInterval 回调中使用 setState 调用另一个状态变量。然后这将导致组件重新呈现。

只有在真正需要的时候才应该使用局部状态的参考文献(即。如果出现流量或性能问题) ,因为它不遵循“ 反应方式”。

  • 计数器应用程序看到 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要么为空,要么为 id。如果我们在这里没有使用 ref,那么每次我们从 null 切换到 id 或者从 id 切换到 null,这个组件都会重新呈现,这将创建一个无限循环。

  • 如果不需要更新 UI,请使用 useRef来存储状态变量。

让我们说,在反应本地应用程序,我们设置了声音的某些行动,对 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,那么每次更改状态值时都会重新呈现。

UseRef ()只更新值而不是重新呈现 UI,如果你想重新呈现 UI,那么你必须使用 useState ()而不是 useRe。如果需要更正,请告诉我。

UseState 和 useRef 之间的主要区别是-

  1. 在组件重新呈现之间,引用的值是 坚持不懈(保持不变) ,

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

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

查看使用代码:

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>;
}

我被渲染 将被控制台记录为仅 一次