Tom
Tom

Reputation: 2645

How to avoid mutations while pushing an array inside an object

I have an object that holds a company and inside a company, there is an array of teams.

  company: {
    teams: [
      {
        name: 'test team',
        description: 'dddd',
        team_manager: null,
        company: '592577d5b591966c8e535865',
        permalink: 'test-team',
        createdAt: '2017-05-30T07:38:58.983Z',
        updatedAt: '2017-05-30T07:38:58.983Z',
        id: '592d219277923054118e7299'
      }
    ],
    name: 'test company2',
    createdAt: '2017-05-24T12:08:53.418Z',
    updatedAt: '2017-05-24T12:08:53.419Z',
    id: '592577d5b591966c8e535865'
  }
}

When a team is added, I am using this reducer to push the team to the array.

case types.ADD_TEAM_SUCCESS :
  return Object.assign({}, state, state.teams.push(action.newTeam));

This works fine, however, in the console I am getting a warning that says:

'Error: A state mutation was detected inside a dispatch, in the path: company.teams.0'

What is the correct way to close the object and push the new team to the array?

Upvotes: 2

Views: 629

Answers (3)

Ivan Mjartan
Ivan Mjartan

Reputation: 987

As describe before Object.assign() does not do a deep clone of your object.

My suggestion is normalize your store and split into two stores Company and Teams.

Than use combineReducers and connect this into one store you will simplify reducers and will make code more readable. Also you can than play with performance.

I mean ... if you will have component just working with company and you will have change in teams you will avoid easily re-render.

Check Dan Abramov grate video tutorial Redux: Normalizing the State Shape

I will do that somehow (sorry for code in typescript)

export interface ICompanyItem {    
    name: string;    
    id: string;
    createdAt: Date;
    updatedAt: Date;    
}

export interface ITeamItem {
    id: string;
    companyId: string
    name: string;
    description: string;
    team_manager: string;
    createdAt: Date;
    updatedAt: Date;
}

export interface ICompaniesState {
    items: { [id: string]: ICompanyItem };
}

export interface ITeamsState {
    items: { [id: string]: ITeamItem };
}


export const teamsReducer: Reducer<ITeamsState> = (state: ITeamsState, action: Action) => {

    if (isActionType(action, AddNewTeamItem)) {
        let itemsClone: { [id: string]: ITeamItem } = { ...state.items, [action.id]: action.team };

        return {
            ...state,
            items: itemsClone
        };
    }

    return state || unloadedState;
};

Upvotes: 0

davidatthepark
davidatthepark

Reputation: 1365

See this link

If you are using es2015, I you can do something like:

return Object.assign({}, state, { teams: [...state.teams, action.newTeam] });

You are pushing an object directly to state.teams. You should make a copy first and use that instead. If you can't use the spread operator, you can slice state.teams into a new variable and push to that instead.

Upvotes: 3

Alp
Alp

Reputation: 29739

The problem is that Object.assign() does not do a deep clone of your object [1] (see "Warning for Deep Clone").

If you don't have any utility libraries in your stack (like lodash) you can use JSON encoding and decoding to get the job done:

case types.ADD_TEAM_SUCCESS:
    let stateCopy = JSON.parse(JSON.stringify(state));
    state.teams.push(action.newTeam);
    return state;

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

Upvotes: 1

Related Questions