Comment mettre à jour une valeur unique à l'intérieur d'un élément de tableau spécifique dans Redux

107

J'ai un problème où le re-rendu de l'état provoque des problèmes d'interface utilisateur et il a été suggéré de ne mettre à jour qu'une valeur spécifique dans mon réducteur pour réduire la quantité de re-rendu sur une page.

c'est un exemple de mon état

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

et je suis en train de le mettre à jour comme ça

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

action.payloadest un tableau entier contenant de nouvelles valeurs. Mais maintenant, j'ai juste besoin de mettre à jour le texte du deuxième élément dans le tableau de contenu, et quelque chose comme ça ne fonctionne pas

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

action.payloadest maintenant un texte dont j'ai besoin pour la mise à jour.

Ilja
la source

Réponses:

58

Vous pouvez utiliser les assistants React Immutability

import update from 'react-addons-update';

// ...    

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

Bien que j'imagine que vous feriez probablement quelque chose de plus comme ça?

case 'SOME_ACTION':
  return update(state, { 
    contents: { 
      [action.id]: {
        text: {$set: action.payload}
      }
    }
  });
Clarkie
la source
en effet, dois-je inclure ces assistants de réaction à l'intérieur de mon réducteur?
Ilja
8
Bien que le paquet ait déménagé kolodny/immutability-helper, il est maintenant yarn add immutability-helperetimport update from 'immutability-helper';
SacWebDeveloper
Mon cas est exactement le même, mais lorsque je mets à jour l'objet dans l'un des éléments du tableau, la valeur mise à jour ne se reflète pas dans le componentWillReceiveProps. J'utilise le contenu directement dans mon mapStateToProps, devons-nous utiliser autre chose dans mapStateToProps pour qu'il se reflète?
Srishti Gupta
173

Vous pouvez utiliser map. Voici un exemple d'implémentation:

case 'SOME_ACTION':
   return { 
       ...state, 
       contents: state.contents.map(
           (content, i) => i === 1 ? {...content, text: action.payload}
                                   : content
       )
    }
Fatih Erikli
la source
c'est comme ça que je fais aussi, je ne suis tout simplement pas sûr que ce soit une bonne approche ou non car la carte renverra un nouveau tableau. Des pensées?
Thiago C. S Ventura
@Ventura oui, c'est bien car cela crée une nouvelle référence pour le tableau et un nouvel objet simple pour celui destiné à être mis à jour (et seulement celui-là), ce qui est exactement ce que vous voulez
ivcandela
3
Je pense que c'est la meilleure approche
Jesus Gomez
12
Je le fais souvent aussi, mais il me semble relativement coûteux de parcourir tous les éléments plutôt que de mettre à jour l'index nécessaire.
Ben Creasy
Jolie ! J'aime cela !
Nate Ben
21

Vous n'êtes pas obligé de tout faire en une seule ligne:

case 'SOME_ACTION':
  const newState = { ...state };
  newState.contents = 
    [
      newState.contents[0],
      {title: newState.contnets[1].title, text: action.payload}
    ];
  return newState
Yuya
la source
1
Vous avez raison, j'ai fait une solution rapide. btw, la longueur du tableau est-elle fixe? et l'index que vous souhaitez modifier est-il également fixe? L'approche actuelle n'est viable qu'avec un petit tableau et un index fixe.
Yuya
2
enveloppez le corps de votre cas dans {}, puisque const a une portée de bloc.
Benny Powers
15

Très tard à la fête mais voici une solution générique qui fonctionne avec chaque valeur d'index.

  1. Vous créez et étendez une nouvelle matrice de l'ancienne matrice à celle que indexvous souhaitez modifier.

  2. Ajoutez les données souhaitées.

  3. Créez et diffusez un nouveau tableau à partir de celui que indexvous vouliez modifier jusqu'à la fin du tableau

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

Mettre à jour:

J'ai fait un petit module pour simplifier le code, il vous suffit donc d'appeler une fonction:

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

Pour plus d'exemples, jetez un œil au référentiel

signature de fonction:

insertIntoArray(originalArray,insertionIndex,newData)
Ivan V.
la source
4

Je crois que lorsque vous avez besoin de ce type d'opérations sur votre état Redux, l'opérateur de diffusion est votre ami et ce principe s'applique à tous les enfants.

Imaginons que c'est votre état:

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

Et vous voulez ajouter 3 points à Serdaigle

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

En utilisant l'opérateur de diffusion, vous ne pouvez mettre à jour que le nouvel état en laissant tout le reste intact.

Exemple tiré de cet article étonnant , vous pouvez trouver presque toutes les options possibles avec d'excellents exemples.

Luis Rodriguez
la source
3

Dans mon cas, j'ai fait quelque chose comme ça, basé sur la réponse de Luis:

...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,
  },
};
Erfan226
la source
0

Voici comment je l'ai fait pour l'un de mes projets:

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;
  }
};
TazExprez
la source
0

J'ai peur que l'utilisation de la map()méthode d'un tableau puisse être coûteuse car tout le tableau doit être itéré. Au lieu de cela, je combine un nouveau tableau composé de trois parties:

  • head - éléments avant l'élément modifié
  • l'élément modifié
  • tail - éléments après l'élément modifié

Voici l'exemple que j'ai utilisé dans mon code (NgRx, pourtant le machanisme est le même pour les autres implémentations 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];
}
Damian Czapiewski
la source