反应:useState 还是 useRef?
React: useState or useRef?
我正在“Hooks FAQ”阅读有关 React useState()
和 useRef()
的文章,我对一些似乎有 useRef 和 useState 解决方案的用例感到困惑同时,我不确定哪种方式是正确的。
来自“Hooks FAQ”about useRef():
"The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class."
使用 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
导致 re-render,useRef
不会。
他们的共同点是,useState
和useRef
都可以在re-render秒后记住他们的数据。因此,如果您的变量决定视图层渲染,请使用 useState
。否则使用 useRef
我建议阅读这篇 article。
基本上,我们在这些情况下使用UseState,其中state的值应该通过重新渲染来更新。
如果您希望您的信息在组件的生命周期内持续存在,您将使用 UseRef,因为它不适用于重新渲染。
如果你存储间隔id,你唯一能做的就是结束间隔。更好的方法是存储状态 timerActive
,这样您就可以在需要时 stop/start 计时器。
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
在您想要跟踪值更改但不想触发重新渲染或 useEffect
时很有用。
大多数用例是当您有一个依赖于值的函数,但该值需要由函数结果本身更新时。
例如,假设您要对某些 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
依赖项,这样我们的组件就不会无限循环了。
您还可以使用 useRef
来引用 dom 元素(默认 HTML 属性)
例如:分配一个按钮以聚焦在输入字段上。
而 useState
仅更新值并重新呈现组件。
这实际上主要取决于您使用计时器的目的,这并不清楚,因为您没有显示组件呈现的内容。
如果你想在你的组件渲染中显示你的计时器的值,你需要使用useState。否则,您的 ref 的更改值将不会导致重新渲染,并且计时器不会在屏幕上更新。
如果必须发生其他事情,应该 在计时器的每个滴答声 视觉上更改 UI ],您使用 useState 并将计时器变量放在 useEffect 挂钩的依赖项数组中(您可以在其中执行 UI 更新所需的任何操作),或者在渲染方法中执行您的逻辑(组件 return 值)基于定时器值。
SetState 调用将导致重新渲染,然后调用您的 useEffect 挂钩(取决于依赖项数组)。
使用 ref,不会发生更新,也不会调用 useEffect。
如果您只想在内部使用定时器,您可以改用 useRef。每当必须发生导致重新渲染的事情时(即经过一定时间后),您就可以在 setInterval 回调中使用 setState 调用另一个状态变量。这将导致组件重新渲染。
仅在真正需要时(即在出现流程或性能问题时)才应使用本地状态的 refs,因为它不遵循“the React way”。
- 计数器 App 看到
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>
该值不会改变,因为组件不是 RE-RENDERING。如果您检查控制台 console.log(count.current)
,您会看到该值实际上在增加,但由于组件没有重新渲染,UI 没有得到更新。
如果您使用 useState
设置状态,则单击按钮会重新呈现组件,因此 UI 会得到更新。
- 防止输入
input
时出现不必要的 re-renderings。
重新渲染是一项昂贵的操作。在某些情况下,您不想继续重新呈现应用程序。例如,当您将输入值存储在状态中以创建受控组件时。在这种情况下,对于每次击键,您都会重新呈现应用程序。如果您使用 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
returns 一个 id,我们将它传递给 clearInterval
以在我们离开组件时结束订阅。 flipInterval.current
为 null 或此 ID。如果我们在这里不使用 ref
,每次我们从 null 切换到 id 或从 id 切换到 null 时,这个组件都会重新渲染,这将创建一个无限循环。
- 如果不需要更新UI,使用
useRef
存储状态变量。
假设在 React Native 应用程序中,我们为某些对 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() 只更新值而不是 re-render 你的 UI 如果你想 re-render UI 那么你必须使用 useState() 而不是 useRe。让我知道是否需要更正。
useState 和 useRef 的主要区别是 -
引用的值在组件re-rendering,
之间persisted(保持不变)
使用 useRef
更新引用不会触发 组件 re-rendering。
但是,更新状态 c 会导致组件 re-rendering
引用更新是同步,更新的引用值是立即可用的,但是状态更新是异步的 - 值在 re-rendering.
后更新
使用代码查看:
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>;
}
每次点击按钮都会显示I rendered!
但是,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>;
}
我被渲染 将被控制台记录一次。
我正在“Hooks FAQ”阅读有关 React useState()
和 useRef()
的文章,我对一些似乎有 useRef 和 useState 解决方案的用例感到困惑同时,我不确定哪种方式是正确的。
来自“Hooks FAQ”about useRef():
"The useRef() Hook isn’t just for DOM refs. The “ref” object is a generic container whose current property is mutable and can hold any value, similar to an instance property on a class."
使用 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
导致 re-render,useRef
不会。
他们的共同点是,useState
和useRef
都可以在re-render秒后记住他们的数据。因此,如果您的变量决定视图层渲染,请使用 useState
。否则使用 useRef
我建议阅读这篇 article。
基本上,我们在这些情况下使用UseState,其中state的值应该通过重新渲染来更新。
如果您希望您的信息在组件的生命周期内持续存在,您将使用 UseRef,因为它不适用于重新渲染。
如果你存储间隔id,你唯一能做的就是结束间隔。更好的方法是存储状态 timerActive
,这样您就可以在需要时 stop/start 计时器。
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
在您想要跟踪值更改但不想触发重新渲染或 useEffect
时很有用。
大多数用例是当您有一个依赖于值的函数,但该值需要由函数结果本身更新时。
例如,假设您要对某些 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
依赖项,这样我们的组件就不会无限循环了。
您还可以使用 useRef
来引用 dom 元素(默认 HTML 属性)
例如:分配一个按钮以聚焦在输入字段上。
而 useState
仅更新值并重新呈现组件。
这实际上主要取决于您使用计时器的目的,这并不清楚,因为您没有显示组件呈现的内容。
如果你想在你的组件渲染中显示你的计时器的值,你需要使用useState。否则,您的 ref 的更改值将不会导致重新渲染,并且计时器不会在屏幕上更新。
如果必须发生其他事情,应该 在计时器的每个滴答声 视觉上更改 UI ],您使用 useState 并将计时器变量放在 useEffect 挂钩的依赖项数组中(您可以在其中执行 UI 更新所需的任何操作),或者在渲染方法中执行您的逻辑(组件 return 值)基于定时器值。 SetState 调用将导致重新渲染,然后调用您的 useEffect 挂钩(取决于依赖项数组)。 使用 ref,不会发生更新,也不会调用 useEffect。
如果您只想在内部使用定时器,您可以改用 useRef。每当必须发生导致重新渲染的事情时(即经过一定时间后),您就可以在 setInterval 回调中使用 setState 调用另一个状态变量。这将导致组件重新渲染。
仅在真正需要时(即在出现流程或性能问题时)才应使用本地状态的 refs,因为它不遵循“the React way”。
- 计数器 App 看到
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>
该值不会改变,因为组件不是 RE-RENDERING。如果您检查控制台 console.log(count.current)
,您会看到该值实际上在增加,但由于组件没有重新渲染,UI 没有得到更新。
如果您使用 useState
设置状态,则单击按钮会重新呈现组件,因此 UI 会得到更新。
- 防止输入
input
时出现不必要的 re-renderings。
重新渲染是一项昂贵的操作。在某些情况下,您不想继续重新呈现应用程序。例如,当您将输入值存储在状态中以创建受控组件时。在这种情况下,对于每次击键,您都会重新呈现应用程序。如果您使用 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
returns 一个 id,我们将它传递给 clearInterval
以在我们离开组件时结束订阅。 flipInterval.current
为 null 或此 ID。如果我们在这里不使用 ref
,每次我们从 null 切换到 id 或从 id 切换到 null 时,这个组件都会重新渲染,这将创建一个无限循环。
- 如果不需要更新UI,使用
useRef
存储状态变量。
假设在 React Native 应用程序中,我们为某些对 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() 只更新值而不是 re-render 你的 UI 如果你想 re-render UI 那么你必须使用 useState() 而不是 useRe。让我知道是否需要更正。
useState 和 useRef 的主要区别是 -
引用的值在组件re-rendering,
之间persisted(保持不变)使用
useRef
更新引用不会触发 组件 re-rendering。 但是,更新状态 c 会导致组件 re-rendering引用更新是同步,更新的引用值是立即可用的,但是状态更新是异步的 - 值在 re-rendering.
后更新
使用代码查看:
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>;
}
每次点击按钮都会显示I rendered!
但是,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>;
}
我被渲染 将被控制台记录一次。