Using Object.assign for a multi-level javascript object

2

Well I am doing 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 only makes a superficial "copy", which means that it only copies a level of my Object, so when trying 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 have to make a deep copy (Deep-copy) of this but I do not know how to do it.

I tried to do it:

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 the right thing to do.

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, in the this.state the object was not being copied completely, I only changed it for this:

this.state={
  author: JSON.parse(JSON.stringify(props.author)),
  errors:{},
  saving: false
};

And it works for me, but what I do not understand is why?

The full code is here

    
asked by Wilfredo 02.08.2017 в 01:16
source

2 answers

2

Simply replace by:

return state.map(author => author.id === action.author.id ? action.author : author);

We map the array of authors and for each one, we compare if the id of the current author belongs to the payload, in the affirmative case, instead of the current author, you return the payload (the edited one).

Update

Actually if you are doing a correct copying in the state:

const author = {
  id: 'cory-house',
  firstName: 'Cory',
  lastName: 'House',
  address: {
    city: 'Caracas'
  },
};

console.log(Object.assign({}, author)); // -> resultado esperado

What you do in componentDidMount is unnecessary; Object.assign copy several levels of depth ; however, you should be careful if the object to be cloned has references because this method only copies values .

componentWillReceiveProps(nextProps) {
  if (this.props.author.id != nextProps.author.id) {
    const author = Object.assign({}, nextProps.author);
    this.setState({author }); // fíjate que habías escrito 'authors'
  }
}

In the previous code you had a typographical error; when updating the status you did it with the key authors ; in fact, the logical thing would be that when you received a new author in the component, it will never replace 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 making an assignment, otherwise you would be saving an " reference " to the object in question:

const author = this.state.author; // mal
const autor = Object.assign({}, this.state.author); // bien
const autor = ...state.author; // es igual, usamos rest spread

The getAuthor function can be reduced to:

const getAuhtor = (authors, authorId) => authors.find(a => a.id === authorId);

Recommendations

If you use Redux, it minimizes the use of component status. In your case, there should not be an author object in the state of the component; instead, you should create a newAuthor object in the store and act directly on it; so that when saving despatches an action with which the reducer would remain as:

case types.AUTHORS_SAVE_IN_PROCESS: {
  return {
    ...state,
    saving: payload,
  };
}
case types.AUTHORS_SAVE_SUCCESS: {
  const authors = ...state.authors;
  authors.push(payload);

  return {
    ...state,
    authors,
  };
}
case types.AUTHORS_SAVE_FAIL: {
  return {
    ...state,
    fail: true,
    message: payload,
  };
}

Try to abstract from the state of forms using redux-form .

    
answered by 02.08.2017 / 03:50
source
-1

I would change the Object.assign({}, action.author) by JSON.parse(JSON.stringify(action.author))

This gives you a deep copy of the object.

slds,

    
answered by 02.08.2017 в 15:57