警告:setState(...):无法在redux / immutable的现有状态转换期间进行更新

10

我出现了以下错误:

警告:setState(...):不能在现有状态转换期间更新(例如在render或另一个组件的构造函数中)。Render方法应该是props和state的纯函数;构造函数副作用是一个反模式,但可以移动到componentWillMount。

我发现原因是:

const mapStateToProps = (state) => {
  return {
    notifications: state.get("notifications").get("notifications").toJS()
  }
}

如果我不返回通知,那么它就可以工作。但是为什么呢?

import {connect} from "react-redux"
import {removeNotification, deactivateNotification} from "./actions"
import Notifications from "./Notifications.jsx"

const mapStateToProps = (state) => {
  return {
    notifications: state.get("notifications").get("notifications").toJS()
  }
}

const mapDispatchToProps = (dispatch) => {
  return {
    closeNotification: (notification) => {
      dispatch(deactivateNotification(notification.id))
      setTimeout(() => dispatch(removeNotification(notification.id)), 2000)
    }
  }
}

const NotificationsBotBot = connect(mapStateToProps, mapDispatchToProps)(Notifications)
export default NotificationsBotBot

import React from "react"

class Notifications extends React.Component {
  render() {
    return (
      <div></div>
    )
  }
}

export default Notifications

更新

在进一步调试后,我发现上述可能并不是根本原因,我可以让通知保持,但需要删除dispatch(push("/domains"))我的重定向。

这是我的登录方式:

export function doLogin (username, password) {
  return function (dispatch) {
    dispatch(loginRequest())
    console.log("Simulated login with", username, password)
    setTimeout(() => {
      dispatch(loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`))
      dispatch(addNotification({
        children: "Successfully logged in",
        type: "accept",
        timeout: 2000,
        action: "Ok"
      }))
      dispatch(push("/domains"))
    }, 1000)
  }
}

我发现派遣引起了警告,但为什么?我的域页面目前没有太多内容:

import {connect} from "react-redux"
import DomainsIndex from "./DomainsIndex.jsx"

export default connect()(DomainsIndex)

域名索引

export default class DomainsIndex extends React.Component {
  render() {
    return (
      <div>
        <h1>Domains</h1>
      </div>
    )
  }
}

更新2

这是我的 App.jsx 文件。 <Notifications /> 是用于显示通知的部分。

  <Provider store={store}>
    <ConnectedRouter history={history}>
      <Layout>
        <Panel>
          <Switch>
            <Route path="/auth" />
            <Route component={TopBar} />
          </Switch>

          <Switch>
            <Route exact path="/" component={Index} />
            <Route path="/auth/login" component={LoginBotBot} />
            <AuthenticatedRoute exact path="/domains" component={DomainsPage} />
            <AuthenticatedRoute exact path="/domain/:id" component={DomainPage} />
            <Route component={Http404} />
          </Switch>
          <Notifications />
        </Panel>
      </Layout>
    </ConnectedRouter>
  </Provider>

3
您能否将代码上传到仓库,让我们玩一下?这个设置有点难以理解。 - fnune
顺便提一下,与其使用 .get().get(),你应该使用 .getIn([])。 - David Bradshaw
1
根据我的经验,这与操作的结构无关,而更多地与触发操作的位置有关。通常,当从componentWillMountrender中触发操作时,您会看到此错误。甚至可能是由于错误,例如在编写onClick={action()}而不是onClick={action}时发生的。 - Sulthan
你能否逐行检查动作创建器 doLogin,看看是哪个方法出了问题,可能是 addNotification 还是 push 方法,目前没有足够的信息来完全理解发生了什么。 - cabolanoz
@cabolanoz 这是推送。没有它,我就收不到警告。 - Jiew Meng
显示剩余2条评论
5个回答

6

您的dispatch(push('/domains'))与其他设置连接组件状态(可能是关心通知的组件)的dispatch一起使用,该组件在push生效后得到重新挂载/卸载。

作为解决方法和概念证明,尝试使用嵌套的零秒setTimeout延迟dispatch(push('/domains'))调用。这样可以在任何其他操作完成之后执行push(即希望渲染后):

setTimeout(() => dispatch(push('/domains')), 0)

如果这样做有效,那么您可能需要重新考虑组件结构。我想 Notifications 是一个您想要挂载一次并在应用程序的整个生命周期内保留在那里的组件。尝试通过将其放置在更高的组件树中并使其成为 PureComponent(这里是文档)来避免重新挂载它。此外,如果您的应用程序复杂度增加,您应该考虑使用处理异步功能的库,例如redux-saga
即使这个警告通常出现在调用操作的错误位置(例如在渲染时调用操作:onClick={action()}而不是传递为lambda:onClick={() => action()}),如果您的组件看起来像您提到的那样(只呈现一个 div),那么这不是问题的原因。

我尝试了0毫秒和1秒的setTimeout,但两者仍然导致相同的错误。我将尝试在我的问题中添加更多细节。 - Jiew Meng
我建议给我们提供一个存储库链接。否则,这个问题很难确定。 - fnune

4
我曾经遇到过这个问题,并使用redux-batched-actions解决了它。
当您在一次性派发多个操作,而不确定更新何时会被触发时,它非常有用。使用此方法,多个操作将只有一个单独的调度。在您的情况下,看起来第二个addNotification没有问题,但第三个有些过多,可能是因为它与历史记录API进行了交互。
我建议您尝试以下代码(当然,假设您的setTimeout将被API调用替换):
import { batchActions } from 'redux-batched-actions'

export function doLogin (username, password) {
  return function (dispatch) {

    dispatch(loginRequest())
    console.log("Simulated login with", username, password)

    setTimeout(() => {

      dispatch(batchActions([
        loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`),
        addNotification({
          children: "Successfully logged in",
          type: "accept",
          timeout: 2000,
          action: "Ok"
        }),
        push("/domains")
      ]))

    }, 1000)
  }
}

请注意,您需要下载该包并在创建存储时启用批处理

1
这是发生的原因,当你调用doLogin时,如果你是从constructor内部调用它。如果是这种情况,请尝试将其移动到componentWillMount,虽然你应该从登录表单中的按钮或输入键调用此方法。
这已经在构造函数不应该被改变中记录。如果这不是问题的根源,你可以在doLogin中注释每一行,以确定哪一行导致状态问题,我猜测可能是pushaddNotification

0
在React组件中,当你调用setState({...})时,它会导致组件重新渲染并调用所有与组件重新渲染相关的生命周期方法,其中render方法就是其中之一。
render中如果你调用setState({...}),它将导致重新渲染并再次调用render,从而触发JavaScript内部的无限循环,最终导致应用程序崩溃。
因此,在任何作为重新渲染过程一部分的生命周期方法中,都不应该调用setState({...})方法,而不仅仅在render中。
在您的情况下,代码可能在渲染的同时触发Redux状态的更新,因此这会导致重新渲染并显示React错误。

0

没有足够的信息给出确定的答案。但可以确定的是,当您尝试在render方法内部调用setState时,会引发此警告。

最常见的情况是,当您调用处理程序函数而不是将它们作为属性传递给子Component时,就会发生这种情况。就像这里这里发生的那样。

因此,我的建议是仔细检查在您的/domains路由上呈现哪些Components以及您如何将onChangeonClick等处理程序传递给它们和它们的子代。


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