未捕获的不变违反: 渲染更多的钩子比在以前的渲染

我有一个这样的组件(非常简化的版本) :

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函数吗?如果能够解释为什么这个修复程序能够工作,那就太好了。

165397 次浏览

The fix works because the first code sample (the erroring one) invokes a function inside onClick, while the second (the working one) passes a function to onClick. The difference is those all-important parentheses, which in JavaScript mean 'invoke this code'.

Think of it this way: in the first code sample, every time component is rendered, renderResults is invoked. Every time that happens, setAllResultsVisible(!allResultsVisible), rather than waiting for a click, is called. Since React performs the render on its own schedule, there's no telling how many times that will happen.

From the React docs:

With JSX you pass a function as the event handler, rather than a string.

React Handling Events Docs

Note: I wasn't able to get this exact error message when running the first code sample in a sandbox. My error referred to an infinite loop. Maybe a more recent version of React produces the error described?

You can simply change your onlick event add () => before setAllResultsVisible

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

and it will work perfectly

Even after the fixes above, there are a few other causes as well for this error. I am writing below one use case which occurred for me.

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

This component can be called in the following ways in 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

See the question can be React :

  1. Rendered lesser hooks than the previous render.
  2. Rendered more hooks than the previous render.

In both the cases thing can be like you have a conditional statement calling the same function which returns render from different places like both wrapped in a parent return function:

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

now function_a() could be a function creating two or one hook suppose useStyle() or anything else

and function_b() could be a function creating no hook.

Now, when parentFunc returns function_a() rendering one hook and function_b() rendering no hook then react will tell you that from the same render function two different renders were returned one with two or one hook and the other with one hook this disparity leads to the error. Error being

less hooks were rendered. And the error is quite obvious.

When cases are reversed and function_b() is returned first cause of the conditionals then react will tell you that from the same render function different renders were returned and error will be .

Rendered more hooks than previous render.

Now, Solution:

Change the code flow like maybe create function_ab() which will ensure all the hooks being used are rendered and in that function:

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

The issue is within the onClick as the setAllResultsVisible is called, it will trigger state change and result on every render

onClick={ setAllResultsVisible(!allResultsVisible) }

Change this to function call instead:

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

I faced the same issue. What I was doing was something like this:

const Table = (listings) => {


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


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


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


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

I think what was happening was that on the first render, the component returned early and the useEffect didn't run. When the isLoading state changed, the useEffect ran and I got the error - the hook rendered more times than the previous render.

A simple change fixed it:

const Table = (listings) => {
    

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

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

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

You have to use your hooks before return in a components

In my case I have used setState() hook inside if condition in the following way, so I got an error after that I have resolved. According to react hook document we should not use hooks inside if condition.

Error:

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

Solution:

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