在渲染不同组件时无法更新组件警告

324

我在React中收到了这个警告:

index.js:1 Warning: Cannot update a component (`ConnectFunction`) 
while rendering a different component (`Register`). To locate the 
bad setState() call inside `Register` 

我去了堆栈跟踪中指定的位置并删除了所有的setstates,但警告仍然存在。这可能是由于redux分发引起的吗?

我的代码:

register.js

class Register extends Component {
  render() {
    if( this.props.registerStatus === SUCCESS) { 
      // Reset register status to allow return to register page
      this.props.dispatch( resetRegisterStatus())  # THIS IS THE LINE THAT CAUSES THE ERROR ACCORDING TO THE STACK TRACE
      return <Redirect push to = {HOME}/>
    }
    return (
      <div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
        <RegistrationForm/>
      </div>
    );
  }
}

function mapStateToProps( state ) {
  return {
    registerStatus: state.userReducer.registerStatus
  }
}

export default connect ( mapStateToProps ) ( Register );

触发在我的registerForm组件中被 register.js 调用的警告的函数。

handleSubmit = async () => {
    if( this.isValidForm() ) { 
      const details = {
        "username": this.state.username,
        "password": this.state.password,
        "email": this.state.email,
        "clearance": this.state.clearance
      }
      await this.props.dispatch( register(details) )
      if( this.props.registerStatus !== SUCCESS && this.mounted ) {
        this.setState( {errorMsg: this.props.registerError})
        this.handleShowError()
      }
    }
    else {
      if( this.mounted ) {
        this.setState( {errorMsg: "Error - registration credentials are invalid!"} )
        this.handleShowError()
      }
    }
  }

堆栈跟踪:

Stacktrace


1
这里有一个关于它的大型线程:https://github.com/facebook/react/issues/18178。您能否发布堆栈跟踪(不是全部,只是相关部分)或告诉我们它所指的确切行? - Red Baron
@RedBaron 添加了堆栈跟踪。 - Tamjid
将回调传递给在父组件中更新状态的子组件没有任何问题。这一直以来都是可以的。这是对我最有帮助的答案:https://stackoverflow.com/a/71257867/470749 - undefined
19个回答

290

自 React V16.3.0 起引入了此警告。

如果您正在使用函数式组件,您可以将 setState 的调用包装到 useEffect 中。

以下代码不起作用:

const HomePage = (props) => {
    
  props.setAuthenticated(true);

  const handleChange = (e) => {
    props.setSearchTerm(e.target.value.toLowerCase());
  };

  return (
    <div key={props.restInfo.storeId} className="container-fluid">
      <ProductList searchResults={props.searchResults} />
    </div>
  );
};

现在你可以将其更改为:

const HomePage = (props) => {
  // trigger on component mount
  useEffect(() => {
    props.setAuthenticated(true);
  }, []);

  const handleChange = (e) => {
    props.setSearchTerm(e.target.value.toLowerCase());
  };

  return (
    <div key={props.restInfo.storeId} className="container-fluid">
      <ProductList searchResults={props.searchResults} />
    </div>
  );
};

43
如果您希望这段代码仅在组件挂载时运行一次,而不是每次状态更新时都运行,请将第二个参数[]添加到useEffect中。 - Grengas
6
这对我有帮助。现在我正在尝试找出原因。有什么帮助吗? - Ken Sawyerr
3
这个链接可能可以回答部分原因,但总体而言,这个问题仍然让我感到困惑,因为我正在尝试解决这个错误。 - tim.rohrer
7
在我的情况下,我通过将 dispatch 移动到 useEffect 中来解决了问题。 - Hamid Shoja
9
你能详细说明为什么这个方法有效吗? - Normal
显示剩余6条评论

74
我刚刚遇到了这个问题,花了一些时间才明白我做错了什么——我没注意到我如何编写我的函数式组件。我是这样做的:
const LiveMatches = (props: LiveMatchesProps) => {
  const {
    dateMatches,
    draftingConfig,
    sportId,
    getDateMatches,
  } = props;

  if (!dateMatches) {
    const date = new Date();
    getDateMatches({ sportId, date });
  };

  return (<div>{component stuff here..}</div>);
};

我刚刚忘记在触发getDateMatches()的redux调用之前使用useEffect了。

所以应该是:

我在getDateMatches()的redux调用之前忘记使用useEffect了。

所以正确的做法是:

const LiveMatches = (props: LiveMatchesProps) => {
  const {
    dateMatches,
    draftingConfig,
    sportId,
    getDateMatches,
  } = props;

  useEffect(() => {
    if (!dateMatches) {
      const date = new Date();
      getDateMatches({ sportId, date });
    }
  }, [dateMatches, getDateMatches, sportId]);

  return (<div>{component stuff here..}</div>);
};

4
将代码放入 useEffect 中是如何消除此警告的? - Plumpie
4
因为 React 是通过“循环”工作的,它会跟踪所呈现的内容并确定何时更新和呈现新组件。它通过使用钩子或生命周期方法来实现这一功能。如果该函数不在 useEffect 钩子中,则组件将尝试在每次重新渲染时调用它,并且这也会与 React 希望的顺序不符地更新组件。将代码移入钩子中,允许 React 按正确的顺序更新组件,以便它可以随着事物的变化进行跟踪。 - Brett East

67
请仔细阅读错误信息。我的错误指向了一个有问题的setState的SignIn组件。我有一个不是箭头函数的onPress

代码如下:

onPress={navigation.navigate("Home", { screen: "HomeScreen" })}

我把它改成了这样:

onPress={() => navigation.navigate("Home", { screen: "HomeScreen" }) }

我的错误信息是:

警告:在渲染不同的组件(SignIn)时,无法更新组件 (ForwardRef(BaseNavigationContainer))。要定位在SignIn中错误的setState()调用,请按照https://reactjs.org/link/setstate-in-render中描述的堆栈跟踪,在SignIn(位于SignInScreen.tsx:20)中查找。


7
当我在半睡半醒状态下编程时,发生了这样的事情。感谢您拯救了我! - hd3adcode
onPress={() => navigation.navigate("HomeScreen")} - ilidiocn
为什么需要箭头函数包装器? - CamHart
@CamHart 简单来说,正如错误信息中所提到的,原因是同时更新不同组件的状态。 箭头函数包装器允许您推迟执行导航函数,直到按下按钮,从而避免与渲染过程中更新状态相关的冲突和错误。 - shiraz27
@CamHart 如果没有它,navigate 函数会在渲染时执行,并将结果分配为 onPress 处理程序。有了它,每次渲染都会分配一个新的函数,在调用时执行 navigate(并将结果返回给调用者)。 - undefined

22
我通过将dispatch从注册组件的render方法中移除到componentwillunmount方法中来解决了这个问题。这是因为我想在重定向到登录页面之前执行此逻辑。一般来说,最好将所有逻辑放在render方法之外,所以我的代码之前写得很糟糕。希望这能帮助将来的任何人 :)

我重构后的注册组件:

class Register extends Component {

  componentWillUnmount() {
    // Reset register status to allow return to register page
    if ( this.props.registerStatus !== "" ) this.props.dispatch( resetRegisterStatus() )
  }

  render() {
    if( this.props.registerStatus === SUCCESS ) { 
      return <Redirect push to = {LOGIN}/>
    }
    return (
      <div style = {{paddingTop: "180px", background: 'radial-gradient(circle, rgba(106,103,103,1) 0%, rgba(36,36,36,1) 100%)', height: "100vh"}}>
        <RegistrationForm/>
      </div>
    );
  }
}


1
你也可以使用一些解构赋值,例如:const {someDispatch} = props;,然后在任何需要的地方使用你的 dispatch。 - Arp

21

我认为这很重要。@Red-Baron指出,这是来自于帖子

@machineghost:我认为你误解了消息的警告内容。

将回调函数传递给在父组件中更新状态的子组件是没有问题的。这一直都没问题。

问题是当一个组件在渲染时,另一个组件队列了一个更新。换句话说,不要这样做:

function SomeChildComponent(props) {
    props.updateSomething();
    return <div />
}

但这没关系:

function SomeChildComponent(props) {
    // or make a callback click handler and call it in there
    return <button onClick={props.updateSomething}>Click Me</button>
}

正如Dan多次指出的那样,在渲染过程中对同一组件进行更新也是可以的:

function SomeChildComponent(props) {
  const [number, setNumber] = useState(0);

  if(props.someValue > 10 && number < 5) {
    // queue an update while rendering, equivalent to getDerivedStateFromProps
    setNumber(42);
  }

  return <div>{number}</div>
}

在我的情况下,我使用 useState 存储回调函数,因为回调函数可能会发生变化。改用 useCallback 解决了这个问题。 - Francisco Gomes

15

如果useEffect不能被使用,或者错误不是因为Redux

我使用setTimeout将两个useState变量之一重定向到回调队列。

我有一个父组件和一个子组件,每个组件中都有一个useState变量。解决方法是使用setTimeout包装useState变量:

setTimeout(() => SetFilterData(data), 0);

下面是一个例子

父组件

import ExpenseFilter from '../ExpensesFilter'
    
function ExpensesView(props) {
    
    const [filterData, SetFilterData] = useState('')
    
    const GetFilterData = (data) => {
       // SetFilterData(data);

       //*****WRAP useState VARIABLE INSIDE setTimeout WITH 0 TIME AS BELOW.*****
       setTimeout(() => SetFilterData(data), 0);
    
    }
    
    const filteredArray = props.expense.filter(expenseFiltered => 
      expenseFiltered.dateSpent.getFullYear().toString() === filterData);
    
    
    return (
    <Window>
      <div>
        <ExpenseFilter FilterYear = {GetFilterData}></ExpenseFilter>

子组件

const ExpensesFilter = (props) => {
    
    const [filterYear, SetFilterYear] = useState('2022')
    
    const FilterYearListener = (event) => {
        event.preventDefault()
        SetFilterYear(event.target.value)
    }
    
    props.FilterYear(filterYear)
    
    return (

3
这段代码之所以可行,是因为setTimeout()是一个阻塞函数,它强制setFilterData函数不再是异步的。这不是状态钩子应该工作的方式,基本上是一种权宜之计,可能会导致意外的行为。 - RyanNerd
2
setImmediate 将是一个很好的替代选择。 - Danny Apostolov
2
根据MDN的说法,setImmediate是非标准的,在面向Web的生产站点上不应使用。 - hkiame
1
@hkiame 抱歉,我一直在想“React Native”...你是完全正确的! - Danny Apostolov
更新:警告!!这种方法会一直调用状态,请尝试在setTimeout中使用console.log(“called the state”)以查看:例如:setTimeout(() => {SetFilterData(data); console.log("called")}, 0); // 打印出 called 999.x 次 - Normal
显示剩余2条评论

6

使用React和Material UI (MUI) 我将我的代码从:

<IconButton onClick={setOpenDeleteDialog(false)}>
        <Close />
      </IconButton>

收件人:

<IconButton onClick={() => setOpenDeleteDialog(false)}>
        <Close />
      </IconButton>

简单修复


1
我曾经遇到过类似的问题,我错误地将一个函数附加到了一个事件上。 - Ivan
2
这里只是一个“我也是”,我在渲染期间执行了处理程序而不是分配它。在我的情况下,我做了类似这样的事情:<button onClick={props.onCancel()} /> 而不是 <button onClick={props.onCancel} />。 - BitsAndBytes

5
如果您使用 React Navigation,并且使用了 setParams 或者 setOptions 方法, 您必须将这些方法放在类组件的 componentDidMount() 方法内或者函数式组件的 useEffect() 钩子中。

4

无法在渲染不同组件时更新组件警告

我遇到了同样的问题,但是当我在渲染的组件内部分派一个操作时。您应该在useEffect钩子内部分派操作以解决该问题


//dispatch action to inform user that 'Marked days already have hours!'
  React.useEffect(() => {
    if (btn_class == 'redButton') {
      dispatch({ type: ActionType.ADD_NOTIFICATION, payload: 'Marked days already have hours!' });
    } else {
      dispatch({ type: ActionType.ADD_NOTIFICATION, payload: '' });
    }
  }, [btn_class, dispatch]);

同时,对于btn-class变量也可以使用联合类型。

type ButtonState = 'btnAddDay' | 'redButton' | 'btnAddDayBlue' | 'btnAddDayGreen';

`*


3
最小复现示例 我有点困惑到底是什么触发了这个问题,有一个最小的可立即运行的示例帮助我更好地理解了它:

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.14.7/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function NotMain(props) {
  props.setN(1)
  return <div>NotMain</div>
}

function Main(props) {
  const [n, setN] = React.useState(0)
  return <>
    <NotMain setN={setN} />
    <div>Main {n}</div>
  </>
}

ReactDOM.render(
  <Main/>,
  document.getElementById('root')
);
</script>
</body>
</html>
失败并出现错误: react-dom.development.js:61 警告:无法在呈现不同组件(`NotMain`)时更新组件(`Main`)。要在 `NotMain` 中定位错误的 `setState()` 调用,请按照堆栈跟踪中的说明 https://reactjs.org/link/setstate-in-render 进行操作。 随后是堆栈跟踪信息:
    at NotMain (<anonymous>:16:9)
    at Main (<anonymous>:21:31)
大概16:9是props.setN(1)被调用的确切行,但由于Babel JSX翻译的原因,行号有点混乱。 像许多其他答案所说的那样,解决方案是改为:
function NotMain(props) {
  React.useEffect(() => { props.setN(1) }, [])
  return <div>NotMain</div>
}
直觉上,我认为此错误发生的一般原因是: 您不应该从渲染方法中更新状态,否则这可能会导致React渲染事物的内部排序方式而产生不同的结果。 并且在使用函数式组件时,解决方法是使用钩子。在我们的情况下,`useEffect`将在渲染完成后运行,因此我们可以从那里进行操作。 当使用类时,这变得稍微更清晰,并且例如在以下位置被要求: 然而,在使用函数式组件时,概念上有些混合,因为组件函数既是渲染,也是设置回调的代码。

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