useState 导致无限渲染 - 理解解决方案
我遇到了一个奇怪的问题,它只出现在几行代码中,我不明白幕后发生了什么。
我有以下 4 行代码:
function FarmerComponent(props) {
let authCtx = useContext(AuthContext)
let usersAndItemsCtx = useContext(usersAndProductsContext)
let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);
let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(false)
console.log(current_logged_user.likedProfilesId)
let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
isCurrentFarmerLikedFlagToggle(flag);
console.log(flag)
这会产生以下错误:
react-dom.development.js:16317 Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop. at renderWithHooks (react-dom.development.js:16317:1) at updateFunctionComponent (react-dom.development.js:19588:1) at beginWork (react-dom.development.js:21601:1) at HTMLUnknownElement.callCallback (react-dom.development.js:4164:1) at Object.invokeGuardedCallbackDev (react-dom.development.js:4213:1) at invokeGuardedCallback (react-dom.development.js:4277:1) at beginWork$1 (react-dom.development.js:27451:1) at performUnitOfWork (react-dom.development.js:26557:1) at workLoopSync (react-dom.development.js:26466:1) at renderRootSync (react-dom.development.js:26434:1)
同时还会多次记录“false”。
现在,如果我将这些行更改为以下内容:
function FarmerComponent(props) {
let authCtx = useContext(AuthContext)
let usersAndItemsCtx = useContext(usersAndProductsContext)
let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);
console.log(current_logged_user.likedProfilesId)
let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
console.log(flag)
let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(flag)
一切都会顺利进行。
为什么会发生这种情况?事实上,我所知道的一切都没有任何变化......那么为什么结果会有如此巨大的差异呢?
问候
de facto, nothing I'm aware of has changed
变化的是,您不再在函数组件的顶层调用状态设置器 (
isCurrentFarmerLikedFlagToggle
)。您无法在函数组件的顶层调用状态设置器(即使您
设置相同的值
,但在您的情况下,您可能不会这样做,它可能在渲染之间发生了变化),因为函数组件是在渲染阶段调用的,这必须是纯粹的(除了安排稍后发生的副作用)。
但有一个更根本的问题:该标志根本不需要处于状态中。将组件接收的状态信息(作为 props、上下文等)复制到组件状态中通常是一种反模式(
更多信息请见此处
)。在您的示例中,您不需要
isCurrentFarmerLikedFlag
作为状态成员;它的值纯粹来自于你的组件已经提供的信息:
authCtx.currentLoggedUserId
和
usersAndItemsCtx.likedProfilesId
:
let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);
let current_logged_user = usersAndItemsCtx.usersVal.find(
(user) => user.id === authCtx.currentLoggedUserId
);
let isCurrentFarmerLikedFlag = current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false;
// ^^^^^^^^^^^^^^^^^^^^^^^^
如果你认为在每次渲染时确定该值所涉及的工作太多,你可以通过
memoize
该值通过
useMemo
(或任何其他各种记忆方法) helpers):
let authCtx = useContext(AuthContext);
let usersAndItemsCtx = useContext(usersAndProductsContext);
let current_logged_user = useMemo(
() => usersAndItemsCtx.usersVal.find((user) => user.id === authCtx.currentLoggedUserId),
[authCtx.currentLoggedUserId, usersAndItemsCtx.usersVal]
);
let isCurrentFarmerLikedFlag = useMemo(
() => (current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false),
[current_logged_user.likedProfilesId]
);
(下面有更多关于该代码的注释。)
回调会计算值,依赖项数组会告诉
useMemo
何时应再次调用回调,因为某些内容已发生更改。您在计算中直接使用的任何内容都应列在依赖项数组中。 (但请注意,无需列出某些内容来自哪个容器;例如,在上面的第一个依赖项数组中,我们有
authCtx.currentLoggeduserId
,而不是同时有
authCtx
和
authCtx.currentLoggeduserId
。我们不关心
authCtx
是否发生了变化,而
authCtx.currentLoggeduserId
没有变化,因为除了
authCtx.currentLoggeduserId
值之外,我们不使用
authCtx
。)
一些旁注:
-
虽然您(当然)可以在自己的代码中使用任何您喜欢的命名约定,但我建议您在编写与他人共享的代码时遵循标准做法。命名状态设置器的标准做法是,它们以状态变量的名称(首字母大写)作为前缀,前缀为单词
set
。因此,如果标志是isCurrentFarmerLikedFlag
,则设置器应为setIsCurrentFarmerLikedFlag
。(另外,isCurrentFarmerLikedFlagToggle
是一个特别有问题的名称,因为它 不 切换值,而是接受要设置的新值 [可能会也可能不会切换。]) -
此代码:
可以更简洁、更地道地写成 (但请继续阅读) :current_logged_user.likedProfilesId.find((id) => id === props.id) ? true : false
任何时候,如果您只想知道 数组是否 具有与您传入的谓词函数匹配的元素,您可以使用current_logged_user.likedProfilesId.some((id) => id === props.id)
some
,而不是find
。 但是 ,由于您实际上根本不需要谓词函数(您只是在检查值是否直接存在),我们可以更进一步使用includes
:current_logged_user.likedProfilesId.includes(props.id)
-
该代码中的所有
let
声明都可以改为const
,我建议您使用const
,因为直接修改它们的值 (isCurrentFarmerLikedFlag = newValue
) 不起作用并且会产生误导。
考虑到所有这些因素:
const authCtx = useContext(AuthContext);
const usersAndItemsCtx = useContext(usersAndProductsContext);
const current_logged_user = useMemo(
() => usersAndItemsCtx.usersVal.find((user) => user.id === authCtx.currentLoggedUserId),
[authCtx.currentLoggedUserId, usersAndItemsCtx.usersVal]
);
const isCurrentFarmerLikedFlag = useMemo(
() => current_logged_user.likedProfilesId.includes(props.id),
[current_logged_user.likedProfilesId]
);
我认为您的问题是在没有任何条件的情况下调用函数
isCurrentFarmerLikedFlagToggle
,因此每次您的组件渲染时,您也会更新状态,从而导致组件一次又一次地渲染。
function FarmerComponent(props) {
let authCtx = useContext(AuthContext)
let usersAndItemsCtx = useContext(usersAndProductsContext)
let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);
let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(false)
console.log(current_logged_user.likedProfilesId)
let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
isCurrentFarmerLikedFlagToggle(flag); // calling the function right after
console.log(flag)
当您没有这样调用它时,它会像下面的代码一样正常工作。
function FarmerComponent(props) {
let authCtx = useContext(AuthContext)
let usersAndItemsCtx = useContext(usersAndProductsContext)
let current_logged_user = usersAndItemsCtx.usersVal.find(user => user.id === authCtx.currentLoggedUserId);
console.log(current_logged_user.likedProfilesId)
let flag = (current_logged_user.likedProfilesId.find((id) =>id === props.id) ? true : false)
console.log(flag)
let [isCurrentFarmerLikedFlag, isCurrentFarmerLikedFlagToggle] = useState(flag) // using the flag for initial value of state in useState