react-redux: Triggering async data request in ES6 "constructor()" or "componentWillMount()" or "componentDidMount()"

(I’m fairly new to React and Redux, and so I’m still trying to figure out the best coding patterns and practices, so apologies if this is obvious to everybody but me.)

I’m working on a React/Redux app which has several components which request their own data. For example, when a “/users” route is invoked, the <Users /> component gets invoked, and that component triggers an async call (via redux-thunk) which updates the Redux store with the fetched user data, and then ultimately the users data appears within the <Users /> component.

The render() function within <Users /> is just watching for changes to this.props. The parent of <Users /> watches the Redux store, and of course passes the user data down.

This all worked fine in React 0.13. Or at least it “worked” with no errors/warnings in the console. I updated to 0.14 late last week, and started to see the following warning in the logs:

Warning: setState(…): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.

I floundered around, and finally figured out that the problem was that my async data request was in the class constructor. Something like this:

constructor(props) {
    super(props);
    fetchUsers();
}

render() {
    return ( do the stuff to output the user data );
}

If I moved the fetchUsers() call to either componentDidMount() or componentWillMount(), everything functioned properly, and the error went away.

So my question is ultimately this: what is going on here? What is the functional difference between these three functions? My (obviously incorrect) assumption was that I should request the data when the class gets initialized, which is why I put the async call in constructor(). Why does that not work, why do those two React lifecycle methods work, and which one of the two is preferable? React’s docs say componentDidMount() is the right method to use, but I would think I’d want the data before the component is mounted, which make me think componentWillMount() should be the method I use.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 25
  • Comments: 24 (11 by maintainers)

Commits related to this issue

Most upvoted comments

My reaction to this is that perhaps I wish there were documentation somewhere about when constructor() and componentWillMount() should be used in React.

Just don’t execute side effects in constructor. It’s only for initializing state (and perhaps other variables). Don’t make calls or change the state of your app from there.

The question is, regarding the sample app I posted, what are the two components in question? You said a dispatch() in one component triggers a setState() in another component. In my example, is that App and Users?

A route change caused dispatch which caused mounting Users which caused another dispatch in its the constructor. So, dispatch inside connect(Users) caused a setState inside connect(App).

And why did dispatch() in one component cause a setState() in another? Is that due to a bad coding pattern, or something internal to React?

Any time you call dispatch(), all connect()-ed component wrappers have their setState() called so that the connected component receive that new state as their props. It’s just how React Redux works.

This is not a bug in React Redux.

This happens when dispatch() inside one component’s constructor causes a setState() inside another component. React keeps track of the “current owner” for such warnings—and it thinks we’re calling setState() inside the constructor when technically constructor causes a setState() inside some other part of the application.

I don’t think we should handle this—it’s just React trying its best do its job. The solution is, as you correctly noted, to dispatch() inside componentWillMount() instead.

Is the warning coming from React Redux’s usage of state? Or are you putting data from store into the state manually? Can you provide an example reproducing the issue?

That said, using componentWillMount is the way to go. Why React doesn’t encourage setState() calls inside constructor, I’m not sure, but it’s better to ask in React issues than here.

Sorry, I meant a unit test.

git clone https://github.com/rackt/react-redux.git
cd react-redux
npm install
npm run test:watch

This should launch our test runner.

Then you can add the test emitting the warning to connect.spec.js (see other tests there for inspiration). To make it easier, you can write it.only() instead of it() so only your test runs.

Does this make sense?

Thanks for the reply, Dan. I posted the question here because the problem seems to be originating in redux-thunk.

I can do my best to piece together an example. This isn’t exactly the code I have executing, but it is, I think, a close enough approximation to illustrate the issue.

index.js:

ReactDOM.render(
    <Provider store={store}>
    <App />
    </Provider>,
    document.getElementById('root')
);

app.js:

render () {
    <GetUsers />
}

export default connect (state => state)(App);

getusers.js

import React, { Component, PropTypes } from 'react';
import {connect} from 'react-redux';
import { fetchUsersFromServer } from "../actions"
class GetUsers extends Component {

constructor (props) {
    super(props);
    this.props.dispatch ( fetchUsersFromServer () );
}

render () {
    return (
        <OutputStuffHere data={this.props.users} />
    );
}
};


export default connect( (state) => {
return {
    users: state.users
}
})(GetUsers);

actions.js

function getUsers (payload) {
return {
    type: GET_USERS,
    payload: payload
}
}

function receiveGetUsers (payload) {
return {
    type: RECEIVE_GET_USERS,
    payload: payload
}
}

export function fetchUsersFromServer () {
return (dispatch) => {
    dispatch ( getUsers() );
            // doesn't matter if you make an async call here or not
            // a single "dispatch()" call will trigger the error
    // dispatch ( receiveGetUsers() );
}
}

reducers.js

function getUsers (state = defaultState.users, action) {
switch (action.type) {
    case GET_USERS:
        return Object.assign({}, state, {
            isFetching: true
        })
    case RECEIVE_GET_USERS:
        if (action.payload.success === true) {  
            return Object.assign ({}, state, {
                isFetching: false,
                courses: action.payload.data
            });
        } else {
            return Object.assign ({}, state, {
                isFetching: false,
                data: {},
                error: {
                    hasError: true,
                    message: action.payload.message
                }
            });
        }
    default:
        return state;
}
}

I hope the sample code is somewhat useful. What I found somewhat interesting was that in the fetchUsersFromServer() call, it didn’t matter if I actually made an async request or not, as I indicate in the comment above. I guess maybe that means this is just me not understanding the React lifecycle properly. Again, I posted this problem here because the problem gets exposed within the thunk, though I realize that doesn’t mean the thunk itself is at fault.

So if the real answer here is "make async calls in componentWillMount(), then I’m happy with that answer. I suppose I’m just trying to get a better understanding of how these parts work together, and why this issue only became apparent when I upgraded to React 0.14.

Thanks again!