开发者问题收集

useState 导致无限渲染 - 理解解决方案

2022-10-15
330

我遇到了一个奇怪的问题,它只出现在几行代码中,我不明白幕后发生了什么。

我有以下 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)

一切都会顺利进行。

为什么会发生这种情况?事实上,我所知道的一切都没有任何变化......那么为什么结果会有如此巨大的差异呢?

问候

2个回答

de facto, nothing I'm aware of has changed

变化的是,您不再在函数组件的顶层调用状态设置器 ( isCurrentFarmerLikedFlagToggle )。您无法在函数组件的顶层调用状态设置器(即使您 设置相同的值 ,但在您的情况下,您可能不会这样做,它可能在渲染之间发生了变化),因为函数组件是在渲染阶段调用的,这必须是纯粹的(除了安排稍后发生的副作用)。

但有一个更根本的问题:该标志根本不需要处于状态中。将组件接收的状态信息(作为 props、上下文等)复制到组件状态中通常是一种反模式( 更多信息请见此处 )。在您的示例中,您不需要 isCurrentFarmerLikedFlag 作为状态成员;它的值纯粹来自于你的组件已经提供的信息: authCtx.currentLoggedUserIdusersAndItemsCtx.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)
    
  1. 该代码中的所有 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]
);
T.J. Crowder
2022-10-15

我认为您的问题是在没有任何条件的情况下调用函数 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
Rohit Bhati
2022-10-15