如何在 redux 中更新特定数组项内的单个值

How to update single value inside specific array item in redux

我遇到了一个问题,即重新呈现状态会导致 ui 问题,并且建议只更新我的 reducer 中的特定值以减少页面上的重新呈现量。

这是我所在州的例子

{
 name: "some name",
 subtitle: "some subtitle",
 contents: [
   {title: "some title", text: "some text"},
   {title: "some other title", text: "some other text"}
 ]
}

我目前正在这样更新它

case 'SOME_ACTION':
   return { ...state, contents: action.payload }

其中 action.payload 是包含新值的整个数组。但现在我实际上只需要更新内容数组中第二项的文本,而这样的事情不起作用

case 'SOME_ACTION':
   return { ...state, contents[1].text: action.payload }

其中 action.payload 现在是我需要更新的文本。

您不必在一行中完成所有操作:

case 'SOME_ACTION': {
  const newState = { ...state };
  newState.contents = 
    [
      newState.contents[0],
      {title: newState.contents[1].title, text: action.payload}
    ];
  return newState
};

您可以使用 React Immutability helpers

import update from 'react-addons-update';

// ...    

case 'SOME_ACTION':
  return update(state, { 
    contents: { 
      1: {
        text: {$set: action.payload}
      }
    }
  });

虽然我想你可能会做更多类似的事情?

case 'SOME_ACTION':
  return update(state, { 
    contents: { 
      [action.id]: {
        text: {$set: action.payload}
      }
    }
  });

您可以使用 map。这是一个示例实现:

case 'SOME_ACTION':
   return { 
       ...state, 
       contents: state.contents.map(
           (content, i) => i === 1 ? {...content, text: action.payload}
                                   : content
       )
    }

派对已经很晚了,但这里有一个适用于每个索引值的通用解决方案。

  1. 您创建一个新数组并将其从旧数组扩展到您要更改的 index

  2. 添加你想要的数据。

  3. 创建一个新数组并将其展开,从您要更改的 index 到数组末尾

let index=1;// probably action.payload.id
case 'SOME_ACTION':
   return { 
       ...state, 
       contents: [
          ...state.contents.slice(0,index),
          {title: "some other title", text: "some other text"},
         ...state.contents.slice(index+1)
         ]
    }

更新:

我做了一个小模块来简化代码,所以你只需要调用一个函数:

case 'SOME_ACTION':
   return {
       ...state,
       contents: insertIntoArray(state.contents,index, {title: "some title", text: "some text"})
    }

有关更多示例,请查看 repository

函数签名:

insertIntoArray(originalArray,insertionIndex,newData)

编辑: 还有Immer.js库可以处理各种值,它们也可以深度嵌套。

我相信当你需要在你的 Redux 状态上进行这种操作时 传播运算符是你的朋友并且这个原则适用于所有 children.

让我们假装这是你的状态:

const state = {
    houses: {
        gryffindor: {
          points: 15
        },
        ravenclaw: {
          points: 18
        },
        hufflepuff: {
          points: 7
        },
        slytherin: {
          points: 5
        }
    }
}

你想给拉文克劳加3分

const key = "ravenclaw";
  return {
    ...state, // copy state
    houses: {
      ...state.houses, // copy houses
      [key]: {  // update one specific house (using Computed Property syntax)
        ...state.houses[key],  // copy that specific house's properties
        points: state.houses[key].points + 3   // update its `points` property
      }
    }
  }

通过使用扩展运算符,您可以只更新新状态,而其他一切都保持不变。

示例取自此 amazing article,您可以找到几乎所有可能的选项以及很好的示例。

在我的例子中,我根据路易斯的回答做了这样的事情:

// ...State object...
userInfo = {
name: '...',
...
}

// ...Reducer's code...
case CHANGED_INFO:
return {
  ...state,
  userInfo: {
    ...state.userInfo,
    // I'm sending the arguments like this: changeInfo({ id: e.target.id, value: e.target.value }) and use them as below in reducer!
    [action.data.id]: action.data.value,
  },
};

这就是我为我的一个项目所做的:

const markdownSaveActionCreator = (newMarkdownLocation, newMarkdownToSave) => ({
  type: MARKDOWN_SAVE,
  saveLocation: newMarkdownLocation,
  savedMarkdownInLocation: newMarkdownToSave  
});

const markdownSaveReducer = (state = MARKDOWN_SAVED_ARRAY_DEFAULT, action) => {
  let objTemp = {
    saveLocation: action.saveLocation, 
    savedMarkdownInLocation: action.savedMarkdownInLocation
  };

  switch(action.type) {
    case MARKDOWN_SAVE:
      return( 
        state.map(i => {
          if (i.saveLocation === objTemp.saveLocation) {
            return Object.assign({}, i, objTemp);
          }
          return i;
        })
      );
    default:
      return state;
  }
};

恐怕使用数组的 map() 方法可能会很昂贵,因为要迭代整个数组。相反,我组合了一个由三部分组成的新数组:

  • head - 修改项
  • 之前的项
  • 修改项
  • tail - 修改项
  • 之后的项

这里是我在代码中使用的示例(NgRx,但其他 Redux 实现的机制相同):

// toggle done property: true to false, or false to true

function (state, action) {
    const todos = state.todos;
    const todoIdx = todos.findIndex(t => t.id === action.id);

    const todoObj = todos[todoIdx];
    const newTodoObj = { ...todoObj, done: !todoObj.done };

    const head = todos.slice(0, todoIdx - 1);
    const tail = todos.slice(todoIdx + 1);
    const newTodos = [...head, newTodoObj, ...tail];
}

这在 redux-toolkit 中非常简单,它使用 Immer 帮助您编写看起来像可变的不可变代码,更简洁易读。

// it looks like the state is mutated, but under the hood Immer keeps track of
// every changes and create a new state for you
state.x = newValue;

因此不必在普通的 redux reducer 中使用扩展运算符

return { 
  ...state, 
  contents: state.contents.map(
      (content, i) => i === 1 ? {...content, text: action.payload}
                              : content
  )
}

您可以简单地重新分配本地值,让 Immer 为您处理剩下的事情:

state.contents[1].text = action.payload;

现场演示

注意数据结构: 在一个项目中我有这样的数据 state:{comments:{items:[{...},{...},{...},...]} 并在 items 中更新一个 item 我这样做

case actionTypes.UPDATE_COMMENT:
  const indexComment = state.comments.items.findIndex( 
    (comment) => comment.id === action.payload.data.id,
  );
  return {
    ...state,
    comments: {
      ...state.comments,
      items: state.comments.items.map((el, index) =>
        index === indexComment ? { ...el, ...action.payload.data } : el,
      ),
    },
  };

Immer.js(一个了不起的 react/rn/redux 友好包)非常有效地解决了这个问题。 redux 存储由不可变数据组成 - immer 允许您干净地更新存储的数据,就好像数据不是不可变的一样。

这是他们的 redux 文档中的示例: (注意 produce() 包裹在该方法周围。这实际上是您的减速器设置中的唯一变化。)

import produce from "immer"

// Reducer with initial state
const INITIAL_STATE = [
    /* bunch of todos */
]

const todosReducer = produce((draft, action) => {
    switch (action.type) {
        case "toggle":
            const todo = draft.find(todo => todo.id === action.id)
            todo.done = !todo.done
            break
        case "add":
            draft.push({
                id: action.id,
                title: "A new todo",
                done: false
            })
            break
        default:
            break
    }
})

(有人提到 immer 是 redux-toolkit 的副作用,但你应该直接在你的 reducer 中使用 immer。)

嵌入式安装: https://immerjs.github.io/immer/installation