React 和 Redux:未捕获的错误:在调度之间检测到状态突变 [英] React and Redux: Uncaught Error: A state mutation was detected between dispatches

查看:32
本文介绍了React 和 Redux:未捕获的错误:在调度之间检测到状态突变的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用受控"组件(在组件内使用 setState())并在尝试保存表单数据时间歇性地收到此错误.UserForm onSave 回调到下面组件代码中的 saveUser 方法.

我已经查看了关于此的 Redux 文档,但无法完全理解我正在修改状态以导致标题中的错误的地方,具体是:Uncaught Error: A state mutation在调度之间检测到,在路径users.2"中.这可能会导致错误的行为.

据我所知,只进行了局部修改,reducer 返回了一个全局状态的副本,并添加了我的更改.我一定错过了什么,但什么?

这是组件代码:

import React, {PropTypes} from 'react';从'react-redux'导入{connect};从redux"导入{bindActionCreators};import * as userActions from '../../actions/userActions';从'./UserForm'导入用户表单;导出类 ManageUserPage 扩展了 React.Component {构造函数(道具,上下文){超级(道具,上下文);this.state = {用户:Object.assign({}, this.props.user),错误:{},储蓄:假};this.updateUserState = this.updateUserState.bind(this);this.saveUser = this.saveUser.bind(this);}componentWillReceiveProps(nextProps) {如果 (this.props.user.id != nextProps.user.id) {这个.setState({用户:Object.assign({}, nextProps.user)});}}更新用户状态(事件){const 字段 = event.target.name;让用户 = Object.assign({}, this.state.user);用户[字段] = event.target.value;返回 this.setState({user: user});}userFormIsValid() {让 formIsValid = true;让错误 = {};如果(this.state.user.firstName.length <3){errors.firstName = '名称必须至少为 3 个字符.';formIsValid = 假;}this.setState({errors: errors});返回表单IsValid;}保存用户(事件){event.preventDefault();如果 (!this.userFormIsValid()) {返回;}this.setState({保存:真});this.props.actions.saveUser(this.state.user).then(() => this.redirect()).catch((错误) => {this.setState({保存:假});});}重定向(){this.setState({保存:假});this.context.router.push('/users');}使成为() {返回 (<用户表单onChange={this.updateUserState}onSave={this.saveUser}错误={this.state.errors}用户={this.state.user}保存={this.state.saving}/>);}}ManageUserPage.propTypes = {用户:PropTypes.object.isRequired,动作:PropTypes.object.isRequired};ManageUserPage.contextTypes = {路由器:PropTypes.object};函数 getUserById(users, userId) {const user = users.find(u => u.id === userId);返回用户 ||空值;}函数 mapStateToProps(state, ownProps) {让用户 = {ID:        '',名: '',姓:  ''};const userId = ownProps.params.id;如果(state.users.length && userId){user = getUserById(state.users, userId);}返回 {用户:用户};}函数 mapDispatchToProps(dispatch) {返回 {动作:bindActionCreators(userActions, dispatch)};}导出默认连接(mapStateToProps,mapDispatchToProps)(ManageUserPage);

这是减速器:

import * as types from '../actions/actionTypes';从 './initialState' 导入初始状态;export default(state = initialState.users, action) =>{开关(动作.类型){案例类型.CREATE_USER_SUCCESS:返回 [//获取我们的状态,然后添加我们的新用户...状态,Object.assign({}, action.user)];案例类型.UPDATE_USER_SUCCESS:返回 [//从我们的状态副本中过滤掉这个用户,然后将我们更新的用户添加到...state.filter(user => user.id !== action.user.id),Object.assign({}, action.user)];默认:返回状态;}};

以下是操作:

import * as types from './actionTypes';从 '../api/mockUserApi' 导入 userApi;从'./ajaxStatusActions'导入{beginAjaxCall,ajaxCallError};导出函数 createUserSuccess(user) {返回 {type: types.CREATE_USER_SUCCESS, user};}导出函数 updateUserSuccess(user) {返回 {type: types.UPDATE_USER_SUCCESS, user};}导出函数 saveUser(user) {返回函数(调度,getState){调度(beginAjaxCall());返回 userApi.saveUser(user).then(savedUser => {用户身份?dispatch(updateUserSuccess(savedUser)): dispatch(createUserSuccess(savedUser));}).catch(错误=>{调度(ajaxCallError(错误));抛出(错误);});};}

这是模拟 API 层:

从'./delay'导入延迟;常量用户 = [{id: '约翰史密斯',firstName: '约翰',姓氏:'史密斯'}];const generateId = (user) =>{返回 user.firstName.toLowerCase() + '-' + user.lastName.toLowerCase();};类 UserApi {静态保存用户(用户){用户 = Object.assign({}, 用户);返回新的承诺((解决,拒绝)=> {setTimeout(() => {const minUserNameLength = 3;if (user.firstName.length < minUserNameLength) {reject(`名字必须至少为 ${minUserNameLength} 个字符.`);}if (user.lastName.length < minUserNameLength) {reject(`Last Name 必须至少为 ${minUserNameLength} 个字符.`);}如果(用户 ID){const existingUserIndex = users.findIndex(u => u.id == u.id);users.splice(existingUserIndex, 1, user);} 别的 {user.id = generateId(user);用户推送(用户);}解决(用户);}, 延迟);});}}导出默认 UserApi;

解决方案

@DDS 为我指明了正确的方向(谢谢!)因为是其他地方的突变导致了问题.

ManageUserPage 是 DOM 中的顶级组件,但另一个名为 UsersPage 的路由上的不同组件在其渲染方法中改变了状态.

最初的渲染方法是这样的:

render() {const users = this.props.users.sort(alphaSort);返回 (<div><h1>用户</h1><输入类型=提交"值="添加用户"className="btn btn-primary"onClick={this.redirectToAddUserPage}/><用户列表用户={用户}/>

);}

我将 users 分配更改为以下内容,问题已解决:

const users = [...this.props.users].sort(alphaSort);

I am using 'controlled' components (using setState() within the component) and getting this error intermittently when attempting to save the form data. The UserForm onSave calls back to the saveUser method in the component code below.

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: Uncaught Error: A state mutation was detected between dispatches, in the path 'users.2'. This may cause incorrect behavior.

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?

Here's the component 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 < 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(() => this.redirect())
      .catch((error) => {
        this.setState({saving: false});
      });
  }

  redirect() {
    this.setState({saving: false});
    this.context.router.push('/users');
  }

  render() {
    return (
      <UserForm
        onChange={this.updateUserState}
        onSave={this.saveUser}
        errors={this.state.errors}
        user={this.state.user}
        saving={this.state.saving}/>
    );
  }
}

ManageUserPage.propTypes = {
  user:  PropTypes.object.isRequired,
  actions: PropTypes.object.isRequired
};

ManageUserPage.contextTypes = {
  router: PropTypes.object
};


function getUserById(users, userId) {
  const user = users.find(u => u.id === userId);
  return user || null;
}

function mapStateToProps(state, ownProps) {
  let user = {
    id:        '',
    firstName: '',
    lastName:  ''
  };

  const userId = ownProps.params.id;

  if (state.users.length && userId) {
    user = getUserById(state.users, userId);
  }


  return {
    user:  user
  };
}

function mapDispatchToProps(dispatch) {
  return {
    actions: bindActionCreators(userActions, dispatch)
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(ManageUserPage);

Here's the reducer:

import * as types from '../actions/actionTypes';
import initialState from './initialState';


export default(state = initialState.users, action) =>
{
  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 => user.id !== action.user.id),
        Object.assign({}, action.user)
      ];

    default:
      return state;
  }
};

Here are the actions:

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 => {
        user.id
          ? dispatch(updateUserSuccess(savedUser))
          : dispatch(createUserSuccess(savedUser));
      }).catch(error => {
        dispatch(ajaxCallError(error));
        throw(error);
      });
  };
}

Here's the mock API layer:

import delay from './delay';

const users = [
  {
    id: 'john-smith',
    firstName: 'John',
    lastName: 'Smith'
  }
];

const generateId = (user) => {
  return user.firstName.toLowerCase() + '-' + user.lastName.toLowerCase();
};

class UserApi {
  static saveUser(user) {
    user = Object.assign({}, user); 
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const minUserNameLength = 3;
        if (user.firstName.length < minUserNameLength) {
          reject(`First Name must be at least ${minUserNameLength} characters.`);
        }

        if (user.lastName.length < minUserNameLength) {
          reject(`Last Name must be at least ${minUserNameLength} characters.`);
        }

        if (user.id) {
          const existingUserIndex = users.findIndex(u => u.id == u.id);
          users.splice(existingUserIndex, 1, user);
        } else {
          user.id = generateId(user);
          users.push(user);
        }
        resolve(user);
      }, delay);
    });
  }
}

export default UserApi;

解决方案

@DDS pointed me in the right direction (thanks!) in that it was mutation elsewhere that was causing the problem.

ManageUserPage is the top-level component in the DOM, but a different component on another route called UsersPage, was mutating state in its render method.

Initially the render method looked like this:

render() {
    const users = this.props.users.sort(alphaSort);
    return (
      <div>
        <h1>Users</h1>
        <input type="submit"
               value="Add User"
               className="btn btn-primary"
               onClick={this.redirectToAddUserPage}/>
        <UserList
          users={users}/>
      </div>
    );
}

I changed the users assignment to the following and the issue was resolved:

const users = [...this.props.users].sort(alphaSort);

这篇关于React 和 Redux:未捕获的错误:在调度之间检测到状态突变的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆