can't change nested object in a reducer even w

2019-07-03 15:48发布

I'm trying to update a state inside a reducer, i know i shouldn't mutate the object or nested objects so I'm using map for arrays or object spread for objects. but it seems i can't really change a value that is deeply nested.

Beside the fact that i can't change the state, i really dislike how the code looks and especially the number of loops i need to do to just change one property. I feel like there is a better, readable and more performant way of doing this.

this is the state:

const items = [{
  name: 'item 1',
  id: 'item1',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag2',
    name: 'tag 2'
  }]
}, {
  name: 'item 2',
  id: 'item2',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag4',
    name: 'tag 4'
  }]
}];

this is the action i'm dispatching:

const action = {
  type: 'CHANGE_TAG_NAME',
  payload: {
    itemId: 'item2',
    tagId: 'tag4',
    newTagName: 'tag 44444'
  }
};

this is the reducer:

const itemsReducer = (state = [], action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const itemIndex = state.findIndex(item => item.id === action.payload.itemId);
        const tagIndex = state[itemIndex].tags.findIndex(t => t.id === action.payload.tagId);
        const nextTag = {
          ...state[itemIndex].tags[tagIndex],
            name: action.payload.newTagName
        };
        const nextTags = [
          ...state[itemIndex].tags.slice(0, tagIndex),
          nextTag,
          ...state[itemIndex].tags.slice(tagIndex + 1, ),
        ];
        const nextItem = {
          ...state[itemIndex],
            tags: nextTags
        };

        const nextState = [
          ...state.slice(0, itemIndex),
          nextItem,
          ...state.slice(itemIndex + 1)
        ];
      }
    default:
      return state;
  }
};

2条回答
forever°为你锁心
2楼-- · 2019-07-03 16:12

Your reducer should work just fine, you just forgot to return nextState in your case block.

As for less iterations i suggests this pattern:
map over the items, if the current item's id is different from the itemId you have in the payload then return it as is.
If the item's id is the same then return a new object and then map over the tags, doing the same condition like you did with the item.
If the tag's id isn't the same as the tagId in the payload return it as is, if it does the same return a new object.

Here is a running example:

const items = [{
  name: 'item 1',
  id: 'item1',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag2',
    name: 'tag 2'
  }]
}, {
  name: 'item 2',
  id: 'item2',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag4',
    name: 'tag 4'
  }]
}];

const action = {
  type: 'CHANGE_TAG_NAME',
  payload: {
    itemId: 'item2',
    tagId: 'tag4',
    newTagName: 'tag 44444'
  }
};

const itemsReducer = (state = [], action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const {
          payload: {
            itemId,
            tagId,
            newTagName
          }
        } = action;
        const nextState = state.map(item => {
          if (item.id !== itemId) return item;
          return {
            ...item,
            tags: item.tags.map(tag => {
              if (tag.id !== tagId) return tag;
              return {
                ...tag,
                name: newTagName
              }
            })
          }
        });
        return nextState;
      }
    default:
      return state;
  }
};

console.log(itemsReducer(items, action));

As for a more readable code, i suggest to use more reducers.
A thumb of rule i use is to create a reducer per entity:
itemsReducer,
itemReducer,
tagsReducer,
tagReducer.

This way each reducer will be responsible for its own data.

Here is a running example:

const items = [{
  name: 'item 1',
  id: 'item1',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag2',
    name: 'tag 2'
  }]
}, {
  name: 'item 2',
  id: 'item2',
  tags: [{
    id: 'tag1',
    name: 'tag 1'
  }, {
    id: 'tag4',
    name: 'tag 4'
  }]
}];

const action = {
  type: 'CHANGE_TAG_NAME',
  payload: {
    itemId: 'item2',
    tagId: 'tag4',
    newTagName: 'tag 44444'
  }
};

const tagReducer = (state = {}, action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const {
          payload: {
            newTagName
          }
        } = action;
        const nextState = {
          ...state,
          name: newTagName
        }
        return nextState;
      }
    default:
      return state;
  }
}

const tagsReducer = (state = [], action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const {
          payload: {
            tagId
          }
        } = action;
        const nextState = state.map(tag => {
          if (tag.id !== tagId) return tag;
          return tagReducer(tag, action);
        });
        return nextState;
      }
    default:
      return state;
  }
}


const itemReducer = (state = {}, action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const nextState = {
          ...state,
          tags: tagsReducer(state.tags, action)
        }
        return nextState;
      }
    default:
      return state;
  }
}

const itemsReducer = (state = [], action) => {
  switch (action.type) {
    case 'CHANGE_TAG_NAME':
      {
        const {
          payload: {
            itemId
          }
        } = action;
        const nextState = state.map(item => {
          if (item.id !== itemId) return item;
          return itemReducer(item, action)
        });
        return nextState;
      }
    default:
      return state;
  }
};

console.log(itemsReducer(items, action));

This pattern is often called reducer composition, and you don't have to include all of them in your root reducer, just use them as external pure functions that will calculate the relevant portion of the state for your other reducers.

查看更多
Ridiculous、
3楼-- · 2019-07-03 16:33

You forgot the key word return

    //.....
    const nextState = [
      ...state.slice(0, itemIndex),
      nextItem,
      ...state.slice(itemIndex + 1)
    ];
    // HERE RETURN                                                                     
查看更多
登录 后发表回答