Warning: setState(…): Cannot update during an exis

2020-07-06 02:43发布

问题:

I am getting the error

Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.

I found the cause to be

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

If I do not return notifications there it works. But why is that?

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

UPDATE

On further debugging I found that, the above may not be the root cause after all, I can have the notifications stay but I need to remove dispatch(push("/domains")) my redirect.

This is how I login:

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

I find that the dispatch causes the warning, but why? My domains page have nothing much currently:

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

export default connect()(DomainsIndex)

DomainsIndex

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

UPDATE 2

My App.jsx. <Notifications /> is what displays the 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>

回答1:

Your dispatch(push('/domains')) comes along other dispatches that set the state for a connected component (presumably one that cares about notifications) that gets remounted/unmounted after the push takes effect.

As a workaround and proof of concept, try defering the dispatch(push('/domains')) call with a nested zero-second setTimeout. This should execute the push after any of the other actions finish (i.e. hopefully a render):

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

If that works, then you might want to reconsider your component structure. I suppose Notifications is a component you want to mount once and keep it there for the lifetime of the application. Try to avoid remounting it by placing it higher in the component tree, and making it a PureComponent (here are the docs). Also, if the complexity of your application increases, you should consider using a library to handle async functionality like redux-saga.

Even though this warning usually appears because of a misplaced action call (e.g. calling an action on render: onClick={action()} instead of passing as a lambda: onClick={() => action()}), if your components look like you've mentioned (just rendering a div), then that is not the cause of the problem.



回答2:

I had this issue in the past, and managed to resolve it using redux-batched-actions.

It's very useful for use-cases like yours when you dispatch multiples actions at once and you're unsure of when the updates will get triggered, with this, there will be only one single dispatch for multiple actions. In your case it seems that the subsequent addNotification is fine, but the third one is too much, maybe because it interacts with the history api.

I would try to do something like (assuming your setTimeout will be replaced by an api call of course):

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

Note that you'll have to download the package and enableBatching your reducer in your store creation.



回答3:

The reason this is happening is when are you calling the doLogin, if you are calling it from within a constructor. If this is the case try moving it to componentWillMount although you should be calling this method from a button or enter hit in the login form.

This have been documented in constructor should not mutate If this is not the root of the problem you mind commented each line in doLogin to know exactly which line giving the state problem, my guess would be either the push or the addNotification



回答4:

There is not enough info to give a certain answer. But what is for sure is that this warning is raised when you tries to setState inside render method.

Most often it happens when you are calling your handler functions instead of passing them as props to child Component. Like it happened here or here.

So my advice is to double check which Components are being rendered on your /domains route and how you are passing onChange, onClick, etc. handlers to them and to their children.



回答5:

In a react component when you call setState({...}), it causes the component to re-render and call all the life cycle methods which are involved in re-rendering of the component and render method is one of them

When in render if you call setState({...}) it will cause a re-render and render will be called again and again hence this will trigger an infinite loop inside the javascript eventually leading to crashing of the app

Hence, not only in render but in any life-cycle method which is a part of re-render process setState({...}) method shouldn't be called.

In your case the code might be triggering an update in the redux state while the code is still rendering and hence this causes re-render and react shows error