未捕获的不变式违规:渲染比上次渲染更多的钩子。

139

我有一个组件,看起来像这样(非常简化的版本):

const component = (props: PropTypes) => {

    const [allResultsVisible, setAllResultsVisible] = useState(false);

    const renderResults = () => {
        return (
            <section>
                <p onClick={ setAllResultsVisible(!allResultsVisible) }>
                    More results v
                </p>
                {
                    allResultsVisible &&
                        <section className="entity-block--hidden-results">
                            ...
                        </section>
                }
            </section>
        );
    };

    return <div>{ renderResults() }</div>;
}

当我加载此组件所在的页面时,会出现以下错误:Uncaught Invariant Violation: Rendered more hooks than during the previous render. 我试图找到这个错误的解释,但是我的搜索没有返回任何结果。
当我稍微修改组件时:
const component = (props: PropTypes) => {

    const [allResultsVisible, setAllResultsVisible] = useState(false);

    const handleToggle = () => {
        setAllResultsVisible(!allResultsVisible);
    }

    const renderResults = () => {
        return (
            <section>
                <p onClick={ handleToggle }>
                    More results v
                </p>
                {
                    allResultsVisible &&
                        <section className="entity-block--hidden-results">
                            ...
                        </section>
                }
            </section>
        );
    };

    return <div>{ renderResults() }</div>;
}

我不再收到那个错误了。是因为我在renderResults返回的jsx中包含了setState函数吗?能否解释一下这个修复方式的原理呢?

10个回答

109

我遇到了同样的问题。我所做的事情类似于以下内容:

const Table = (listings) => {

    const {isLoading} = useSelector(state => state.tableReducer);

    if(isLoading){
        return <h1>Loading...</h1>
    }

    useEffect(() => {
       console.log("Run something")
    }, [])

    return (<table>{listings}</table>)
}

我认为发生的情况是在第一次渲染时,组件提前返回并且 useEffect 没有运行。当 isLoading 状态改变时,useEffect 运行了,导致错误 - 这个钩子比上一个渲染多次渲染了。

一个简单的更改就解决了这个问题:

const Table = (listings) => {
    
    const {isLoading} = useSelector(state => state.tableReducer);
        
    useEffect(() => {
        console.log("Run something")
    }, [])
    
    if(isLoading){
        return <h1>Loading...</h1>
    }
    return (<table>{listings}</table>)
}

1
遇到了这个错误,我的问题也是一样的,我在钩子上面有一个return语句。 - jlhasson
这很有帮助。我的钩子在if语句中定义。我把它放在上面,问题就解决了。 - AlbatrossCafe
这对我帮助很大。正是我所需要的! - undefined

63
修复的原因是第一个代码示例(出现错误的那个)在onClick内调用了一个函数,而第二个示例(运行正常的)将一个函数传递给onClick。区别在于那些非常重要的括号,在JavaScript中表示“调用此代码”。
可以这样想:在第一个代码示例中,每次渲染component时,renderResults都会被调用。每次发生这种情况,都会调用setAllResultsVisible(!allResultsVisible),而不是等待单击。由于React按照自己的时间表执行渲染,无法确定会发生多少次。
来自React文档的说明:

使用JSX时,将函数作为事件处理程序传递,而不是字符串。

React Handling Events Docs

注意:在沙箱中运行第一个代码示例时,我无法获得这个精确的错误消息。我的错误引用了一个无限循环。也许React的更近期版本会产生上述所描述的错误?

2
我的错,犯了一个很严重的错误。我总是混淆 () => function()function() 在 onClick 事件中的使用。 - timothym

23

您可以简单地更改您的onclick事件,只需在setAllResultsVisible之前添加() =>


<p onClick={() => setAllResultsVisible(!allResultsVisible) }> 
    More results v
</p>

它将完美地运作。


11

即使进行了以上修复,仍然存在一些其他原因导致此错误。我将在下面写出一个我遇到的用例。

function Comp(props){return <div>{props.val}</div>}

这个组件可以用以下方式在JSX中调用:

1. <Comp val={3} /> // works well
2. { Comp({val:3}) } // throws uncaught invariant violation error, at least it throw in my case, may be there were other factors, but changing back to first way removed that problem

这也解决了我的问题。有人能解释一下为什么会引起错误吗? - finn
我们可以假设,<Comp /> 是JSX,并被理解为React组件,而{Comp(..}虽然做同样的事情,但不被解释为React组件,因此不会进行额外的优化。 - Akshay Vijay Jain

10

看到这个问题可以是React:

  1. 渲染的Hooks比上一次渲染的少。
  2. 渲染的Hooks比上一次渲染的多。

在这两种情况下,可能会出现一个条件语句调用相同函数的情况,该函数从不同位置返回渲染结果,例如都包装在父级return函数中:

const parentFunc = () => {
    if(case==1)
        return function_a();
    if (case==2)
        return function_b();
}

现在,function_a()可以是一个创建一个或两个钩子的函数,例如useStyle()或其他内容。

而function_b()可以是一个不创建钩子的函数。

现在,当parentFunc返回function_a()渲染一个或两个钩子和function_b()不渲染钩子时,React会告诉你从同一个渲染函数返回了两个不同的渲染结果,一个渲染一个或两个钩子,另一个没有钩子,这种差异会导致错误。错误信息为:

渲染的hooks数量较少。这个错误很明显。

当情况反转并且由于条件语句返回了function_b(),则React将告诉你从同一个渲染函数返回了不同的渲染结果,错误将是:

渲染的hooks数量比之前更多。

现在,解决方法是:

改变代码流程,例如创建一个名为function_ab()的函数,该函数将确保所有使用的钩子都被渲染,并且在该函数中:

const function_ab = () => {
    if(case==1)
         return (<div></div>) //or whatever
    if(case==2)
         return (<div>I am 2 </div>) //or whatever
}

3
这个错误的一个可能原因是使用了
Component() // error
<Component /> // good

这是解释:
如果直接调用组件函数,它只是一个返回JSX的函数(没有状态,没有副作用)。内部的hook不会正常工作,可能会被视为父组件的hook。如果使用,相当于执行React.createElement,内部的hook将按预期工作。

2

在我的情况下,我已经使用了如下方式的setState() hook来作为if条件的一部分,因此我在此之后遇到了一个错误,但是我已经解决了。根据React Hook文档,我们不应该在if条件中使用hooks。

错误:

import React, { useState, useCallback } from 'react';
import './style.css';

export default function App() {
  const [count, setCount] = useState(0);
if(count < 10){
  return (
    <div>
      <h1>Hello Count!</h1>
        <button onClick={useCallback(setCount((count) => count + 1))}>
          click to add
        </button>
    </div>
  );
} else {
        return <div>Count reached 10!</div>
    }
 }

解决方案:

import React, { useState, useCallback } from 'react';
import './style.css';

export default function App() {
  const [count, setCount] = useState(0);
  const handleIncrement = useCallback(() => {
        setCount((count) => count + 1)
  })
  
if(count < 10){
return (
    <div>
      <h1>Hello Count!</h1>
      <button onClick={handleIncrement}>click to add</button>
    </div>
  );
} else {
        return <div>Count reached 10!</div>
    }
}

-1

我认为这可能被归类为一个 bug,但很想听听开发人员的想法。基本上,当从 0 个 hook 转换为正数个 hook 时,似乎不会抛出“渲染的 hook 数量比上一次渲染多”的错误。

例如,当组件从渲染 1 个 hook 变为渲染 2 个 hook 时,就会抛出该错误。

React 版本:17.0.2


你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community
文本从https://github.com/facebook/react/issues/23182复制 - Eric Aya
抱歉,我们不再接受您的帐户答案,因为您的大多数答案需要改进或未能充分回答问题。请查看帮助中心以了解更多信息。 - thien

-2
问题出在 onClick 中,因为调用了 setAllResultsVisible,它会触发状态改变并在每次渲染时产生结果。
onClick={ setAllResultsVisible(!allResultsVisible) }

将此更改为函数调用:

onClick={_ => setAllResultsVisible(!allResultsVisible) }

-3

在组件中,你必须在返回之前使用你的钩子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接