Well, I am making a React-Readux application, and I have the following Array of objects that my store has:
const authors = [
{
id: 'cory-house',
firstName: 'Cory',
lastName: 'House',
address: {
city: 'Caracas'
}
},
{
id: 'scott-allen',
firstName: 'Scott',
lastName: 'Allen',
address: {
city: 'Caracas'
}
},
{
id: 'dan-wahlin',
firstName: 'Dan',
lastName: 'Wahlin',
address: {
city: 'Caracas'
}
}
];
When I do an update, in my reducer, I have the following:
import * as types from '../actions/actionTypes';
import initialState from './initialState';
export default function authorReducer(state = initialState.authors, action) {
console.log('actions ', action);
console.log('state ', state);
switch (action.type) {
case types.LOAD_AUTHORS_SUCCESS:
return action.authors;
case types.CREATE_AUTHOR_SUCCESS:
return [
...state,
Object.assign({}, action.author)
];
case types.UPDATE_AUTHOR_SUCCESS:
return [
...state.filter(author => author.id !== action.author.id),
Object.assign({}, action.author)
];
case types.DELETE_AUTHOR_SUCCESS:
var existingAuthorIndex = state.findIndex(a => a.id == action.author.id);
console.log('Index ', existingAuthorIndex);
return [
...state.filter(author => author.id !== action.author.id)
]
default:
return state;
}
}
The Object.assign that I have in UPDATE_AUTHOR_SUCCESS
it only does a shallow "copy", which means that it only copies one level of my Object, so when I try to modify the address field, it throws me the following error:
browser.js?9520:40 Uncaught Error: A state mutation was detected between dispatches, in the path
authors.0.address.city
. This may cause incorrect behavior.
The whole problem is because I should make a deep-copy of this but I don't know how to do it.
I have tried to do:
case types.UPDATE_USERS:
console.log('My uaser ', action);
return [
...state.filter(user => user.id !== action.user.id),
Object.assign({}, action.user, action.user.address)
];
But it still fails me, since it is not correct.
Update
For some reason in the component where I manage my update I had it like this:
import React, {PropTypes} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import AuthorForm from './AuthorForm';
import * as authorActions from '../../actions/authorActions';
import toastr from 'toastr';
export class AuthorManagePage extends React.Component{
constructor(props, context){
super(props, context);
this.state={
author: Object.assign({}, props.author),
errors:{},
saving: false
};
this.saveAuthor = this.saveAuthor.bind(this);
this.updateAuthorState = this.updateAuthorState.bind(this);
}
componentWillReceiveProps(nextProps) {
if (this.props.author.id != nextProps.author.id) {
// Necessary to populate form when existing course is loaded directly.
var clone = Object.assign({}, nextProps.author)
clone.address = Object.assign({}, nextProps.author.address);
this.setState({authors: clone});
}
}
updateAuthorState(event) {
// const field = event.target.name;
// let author = this.state.author;
// author[field] = event.target.value;
// return this.setState({author: author});
const hasSubField = event.target.name.split('.');
console.log(hasSubField)
if (hasSubField.length > 1) {
let author = this.state.author;
author[hasSubField[0]][hasSubField[1]] = event.target.value;
return this.setState({author:author});
}
// else if(hasSubField.length > 2){
// // let user = this.state.user;
// // user.address.geo.lat = event.target.value;
// // return this.setState({user:user});
//
//
// let user = Object.assign({}, this.state.user);
// user.address.geo.lat = event.target.value;
// return this.setState({user:user});
//
// }
else {
const field = event.target.name;
let author = this.state.user;
author[field] = event.target.value;
return this.setState({author:author });
}
}
redirect() {
this.setState({saving: false});
toastr.success('Author saved');
this.context.router.push('/authors');
}
saveAuthor(event){
event.preventDefault();
this.setState({saving: true});
var author = this.state.author;
this.props.actions.saveAuthor(this.state.author).then(() => this.redirect())
.catch(error => {
toastr.error(error);
this.setState({saving: false});
});
}
render(){
return(
<AuthorForm
author={this.state.author}
onSave={this.saveAuthor}
errors={this.state.errors}
saving={this.state.saving}
onChange={this.updateAuthorState}
/>
);
}
}
function getAuthor(authors, authorId){
const author = authors.filter(author => author.id === authorId);
if(author.length) return author[0];
return null;
}
AuthorManagePage.propTypes = {
author: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
};
//Pull in the React Router context so router is available on this.context.router.
AuthorManagePage.contextTypes = {
router: PropTypes.object
};
function mapStateToProps(state, ownProps) {
let authorId = ownProps.params.id;
let author = {id: '', firstName: '', lastName: '', address: { city: ''}};
if (authorId && state.authors.length > 0) {
author = getAuthor(state.authors, authorId);
}
return {
author: author
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(authorActions, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AuthorManagePage);
And I realized that in the constructor, the this.state
entire object was not being copied, I just changed it to this:
this.state={
author: JSON.parse(JSON.stringify(props.author)),
errors:{},
saving: false
};
And it works for me, but what I don't understand is why?
Simply replace with:
We map the array of authors and for each one, we compare if the id of the current author belongs to the payload, if so, instead of the current author we return the payload (the edited one).
Update
Actually if you are doing a correct copy in the state:
What you do in
componentDidMount
is unnecessary;Object.assign
copies several levels deep ; however, you should be careful if the object to be cloned has references as this method only copies values .In the code above you had a typo; when updating the state you did it with the authors key ; in fact, the logical thing would be that when you receive a new author in the component, it never replaces the current one.
Other problems I see is that in the code to update and save, for example, you should clone the author of the state instead of doing an assignment, otherwise you would be saving a "reference" to the object in question:
The function
getAuthor
can be reduced to:recommendations
If you use Redux, minimize the use of component state. In your case, there shouldn't be an author object in the component state; instead, you should create an object
newAuthor
in the store and act on it directly; so that when saving you dispatch an action with which the reducer would be as:Try to abstract away from form state using redux-form .
I would change the
Object.assign({}, action.author)
forJSON.parse(JSON.stringify(action.author))
This makes you a deep copy of the object.
slds,