Как обновить одно значение внутри определенного элемента массива в redux

107

У меня проблема, из-за которой повторный рендеринг состояния вызывает проблемы с пользовательским интерфейсом, и мне было предложено обновить только определенное значение внутри моего редуктора, чтобы уменьшить количество повторного рендеринга на странице.

это пример моего состояния

{
 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теперь текст, который мне нужно обновить.

Илья
источник

Ответы:

60

Вы можете использовать помощники React Immutability

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}
      }
    }
  });
Кларки
источник
действительно, мне нужно как-то включать этих помощников из реакции внутри моего редуктора?
Ilja
8
Хотя пакет переместился в kolodny/immutability-helper, так что теперь он yarn add immutability-helperиimport update from 'immutability-helper';
SacWebDeveloper
Мой случай точно такой же, но когда я обновляю объект в одном из элементов массива, обновленное значение не отражается в componentWillReceiveProps. Я использую содержимое непосредственно в моем mapStateToProps, нужно ли нам использовать что-то еще в mapStateToProps, чтобы оно отражалось?
Сришти Гупта
173

Вы можете использовать map. Вот пример реализации:

case 'SOME_ACTION':
   return { 
       ...state, 
       contents: state.contents.map(
           (content, i) => i === 1 ? {...content, text: action.payload}
                                   : content
       )
    }
Фатих Эрикли
источник
я тоже так поступаю, я просто не уверен, хороший это подход или нет, так как map вернет новый массив. Есть предположения?
Thiago C. S Ventura
@Ventura: да, это хорошо, потому что он создает новую ссылку для массива и простой новый объект для объекта, предназначенного для обновления (и только этого), что именно то, что вы хотите
ivcandela
3
Я думаю, что это лучший подход
Хесус Гомес
12
Я тоже часто это делаю, но кажется относительно дорогостоящим с точки зрения вычислений перебор всех элементов вместо обновления необходимого индекса.
Бен
красотка ! Я люблю это !
Нейт Бен
21

Необязательно делать все в одной строке:

case 'SOME_ACTION':
  const newState = { ...state };
  newState.contents = 
    [
      newState.contents[0],
      {title: newState.contnets[1].title, text: action.payload}
    ];
  return newState
Юя
источник
1
Вы правы, я быстро исправил. кстати, фиксированная длина массива? и индекс, который вы хотите изменить, тоже фиксирован? Текущий подход возможен только с небольшим массивом и с фиксированным индексом.
Yuya
2
оберните тело случая в {}, поскольку область действия const ограничена блоком.
Бенни Пауэрс
15

Очень поздно для вечеринки, но вот общее решение, которое работает со всеми значениями индекса.

  1. Вы создаете и распространяете новый массив из старого массива до того, indexкоторый хотите изменить.

  2. Добавьте нужные данные.

  3. Создайте и распределите новый массив из того, что indexвы хотите изменить, до конца массива

let index=1;// probabbly 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"})
    }

Дополнительные примеры см. В репозитории

сигнатура функции:

insertIntoArray(originalArray,insertionIndex,newData)
Иван В.
источник
4

Я считаю, что когда вам нужны такие операции с вашим состоянием Redux, оператор распространения - ваш друг, и этот принцип применим ко всем детям.

Представим, что это ваше состояние:

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
      }
    }
  }

Используя оператор распространения, вы можете обновить только новое состояние, оставив все остальное нетронутым.

Пример взят из этой замечательной статьи , вы можете найти практически все возможные варианты с отличными примерами.

Луис Родригес
источник
3

В моем случае я сделал что-то вроде этого, основываясь на ответе Луиса:

...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,
  },
};
Эрфан226
источник
0

Вот как я это сделал для одного из своих проектов:

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;
  }
};
ТазЭкспрез
источник
0

Боюсь, что использование map()метода массива может быть дорогостоящим, поскольку нужно повторять весь массив. Вместо этого я объединяю новый массив, состоящий из трех частей:

  • head - элементы перед измененным элементом
  • измененный элемент
  • хвост - элементы после измененного элемента

Вот пример, который я использовал в своем коде (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];
}
Дамиан Чапевски
источник