title: React and Redux: Uncaught Error: A state mutation was detected between dispatches
body: <p>I am using 'controlled' components (using <code>setState()</code> within the component) and getting this error intermittently when attempting to save the form data.  The <code>UserForm</code> <code>onSave</code> calls back to the <code>saveUser</code> method in the component code below.  </p>
  
  <p>I've looked at the Redux docs on this and can't quite get my head around where I'm modifying the state to cause the error in the title, which is specifically: <code>Uncaught Error: A state mutation was detected between dispatches, in the path 'users.2'. This may cause incorrect behavior.</code></p>
  
  <p>As far as I can tell, only local modifications are being made, and the reducer is returning a copy of the global state with my changes added.  I must be missing something, but what? </p>
  
  <p>Here's the component code:</p>
  
  <pre><code>import React, {PropTypes} from 'react';
  import {connect} from 'react-redux';
  import {bindActionCreators} from 'redux';
  import * as userActions from '../../actions/userActions';
  import UserForm from './UserForm';
  
  
  export class ManageUserPage extends React.Component {
    constructor(props, context) {
      super(props, context);
  
      this.state = {
        user:  Object.assign({}, this.props.user),
        errors:  {},
        saving:  false
      };
      this.updateUserState = this.updateUserState.bind(this);
      this.saveUser = this.saveUser.bind(this);
    }
  
    componentWillReceiveProps(nextProps) {
      if (this.props.user.id != nextProps.user.id) {
        this.setState(
          {
            user:  Object.assign({}, nextProps.user)
          }
        );
      }
    }
  
    updateUserState(event) {
      const field = event.target.name;
      let user = Object.assign({}, this.state.user);
      user[field] = event.target.value;
      return this.setState({user: user});
    }
  
    userFormIsValid() {
      let formIsValid = true;
      let errors = {};
  
      if (this.state.user.firstName.length &lt; 3) {
        errors.firstName = 'Name must be at least 3 characters.';
        formIsValid = false;
      }
      this.setState({errors: errors});
      return formIsValid;
    }
  
  
    saveUser(event) {
      event.preventDefault();
      if (!this.userFormIsValid()) {
        return;
      }
      this.setState({saving: true});
      this.props.actions
        .saveUser(this.state.user)
        .then(() =&gt; this.redirect())
        .catch((error) =&gt; {
          this.setState({saving: false});
        });
    }
  
    redirect() {
      this.setState({saving: false});
      this.context.router.push('/users');
    }
  
    render() {
      return (
        &lt;UserForm
          onChange={this.updateUserState}
          onSave={this.saveUser}
          errors={this.state.errors}
          user={this.state.user}
          saving={this.state.saving}/&gt;
      );
    }
  }
  
  ManageUserPage.propTypes = {
    user:  PropTypes.object.isRequired,
    actions: PropTypes.object.isRequired
  };
  
  ManageUserPage.contextTypes = {
    router: PropTypes.object
  };
  
  
  function getUserById(users, userId) {
    const user = users.find(u =&gt; u.id === userId);
    return user || null;
  }
  
  function mapStateToProps(state, ownProps) {
    let user = {
      id:        '',
      firstName: '',
      lastName:  ''
    };
  
    const userId = ownProps.params.id;
  
    if (state.users.length &amp;&amp; userId) {
      user = getUserById(state.users, userId);
    }
  
  
    return {
      user:  user
    };
  }
  
  function mapDispatchToProps(dispatch) {
    return {
      actions: bindActionCreators(userActions, dispatch)
    };
  }
  
  export default connect(mapStateToProps, mapDispatchToProps)(ManageUserPage);
  </code></pre>
  
  <p>Here's the reducer:</p>
  
  <pre><code>import * as types from '../actions/actionTypes';
  import initialState from './initialState';
  
  
  export default(state = initialState.users, action) =&gt;
  {
    switch (action.type) {
      case types.CREATE_USER_SUCCESS:
        return [
          // grab our state, then add our new user in
          ...state,
          Object.assign({}, action.user)
        ];
  
      case types.UPDATE_USER_SUCCESS:
        return [
          // filter out THIS user from our copy of the state, then add our updated user in
          ...state.filter(user =&gt; user.id !== action.user.id),
          Object.assign({}, action.user)
        ];
  
      default:
        return state;
    }
  };
  </code></pre>
  
  <p>Here are the actions:</p>
  
  <pre><code>import * as types from './actionTypes';
  import userApi from '../api/mockUserApi';
  import {beginAjaxCall, ajaxCallError} from './ajaxStatusActions';
  
  
  export function createUserSuccess(user) {
    return {type: types.CREATE_USER_SUCCESS, user};
  }
  
  export function updateUserSuccess(user) {
    return {type: types.UPDATE_USER_SUCCESS, user};
  }
  
  export function saveUser(user) {
    return function (dispatch, getState) {
      dispatch(beginAjaxCall());
      return userApi.saveUser(user)
        .then(savedUser =&gt; {
          user.id
            ? dispatch(updateUserSuccess(savedUser))
            : dispatch(createUserSuccess(savedUser));
        }).catch(error =&gt; {
          dispatch(ajaxCallError(error));
          throw(error);
        });
    };
  }
  </code></pre>
  
  <p>Here's the mock API layer:</p>
  
  <pre><code>import delay from './delay';
  
  const users = [
    {
      id: 'john-smith',
      firstName: 'John',
      lastName: 'Smith'
    }
  ];
  
  const generateId = (user) =&gt; {
    return user.firstName.toLowerCase() + '-' + user.lastName.toLowerCase();
  };
  
  class UserApi {
    static saveUser(user) {
      user = Object.assign({}, user); 
      return new Promise((resolve, reject) =&gt; {
        setTimeout(() =&gt; {
          const minUserNameLength = 3;
          if (user.firstName.length &lt; minUserNameLength) {
            reject(`First Name must be at least ${minUserNameLength} characters.`);
          }
  
          if (user.lastName.length &lt; minUserNameLength) {
            reject(`Last Name must be at least ${minUserNameLength} characters.`);
          }
  
          if (user.id) {
            const existingUserIndex = users.findIndex(u =&gt; u.id == u.id);
            users.splice(existingUserIndex, 1, user);
          } else {
            user.id = generateId(user);
            users.push(user);
          }
          resolve(user);
        }, delay);
      });
    }
  }
  
  export default UserApi;
  </code></pre>