2
votes

Comment prévenir défensivement la mutation de l'état Redux?

J'essaie de coder les réducteurs dans mon application de manière un peu plus défensive, car mon équipe utilise le code préexistant comme modèle pour les nouvelles modifications. Pour cette raison, j'essaie de couvrir tous les écarts potentiels par rapport à l'immuabilité.

Supposons que vous ayez un état initial qui ressemble à ceci:

function foo(dispatch) {
  const abc = { a: 0, b: 0, c: 0 };
  dispatch({ type: SOME_ACTION, payload: { abc } });

  // The following changes the state without dispatching a new action
  // and does so by breaking the immutability of the state too.
  abc.c = 5;
}

Et une gestion du réducteur une action comme celle-ci:

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return { ...state, section1: action.payload.abc };
  }
  return state;
}

Ensuite, vous pourriez avoir un répartiteur qui fait ce qui suit:

const initialState = {
  section1: { a: 1, b: 2, c: 3 },
  section2: 'Some string',
};

Dans ce cas, tandis que le réducteur suit les modèles d'immuabilité en créant une copie superficielle de l'ancien état et en ne modifiant que le bit qui a changé, le répartiteur a toujours accès à action.payload.abc et peut le muter. p >

Peut-être que redux crée déjà une copie complète de toute l'action, mais n'a trouvé aucune source mentionnant cela. J'aimerais savoir s'il existe une approche pour résoudre ce problème simplement.


2 commentaires

Avez-vous envisagé d'écrire votre propre middleware de base pour le gérer?


J'avais envisagé de créer un middleware qui clonait en profondeur l'action avant de la transmettre, mais je voulais voir s'il y avait d'autres solutions toutes faites. Le middleware que j'ai décrit semble cependant très inefficace.


3 Réponses :


0
votes

Je pense qu'au sein de votre réducteur, si vous créez un nouvel objet à partir de action.payload.abc, vous pouvez être sûr que toute modification de l'objet d'origine n'aura aucun impact sur le magasin de redux.

case SOME_ACTION:
      return { ...state, section1: {...action.payload.abc}};


2 commentaires

Droite! Je me demandais une solution ou une approche plus générale ici. Sinon, le modèle de code n'est pas clair quant à la raison pour laquelle cela est parfois fait et d'autres développeurs de l'équipe pourraient ne pas se souvenir de faire la même chose.


Ah! Dans ce cas, la réponse de nem035 est très utile



5
votes

Il convient de noter que dans votre exemple, la mutation ne posera aucun problème si vous effectuez simplement une copie de niveau appropriée en fonction de l'objet.

Pour abc ressemblant à

code> {a: 1, b: 2, c: 3} vous pouvez simplement faire une copie superficielle tandis que pour un objet imbriqué {a: {name: 1}} vous vous devez le copier en profondeur, mais vous pouvez toujours le faire explicitement sans aucune bibliothèque ou quoi que ce soit.

import produce from 'immer'

export default = (state = initialState, action) =>
  produce(state, draft => {
    switch (action.type) {
      case SOME_ACTION:
        draft.section1 = action.section1;
    })
}

Vous pouvez également empêcher la mutation avec un eslint-plugin-immutable qui forcerait le programmeur à ne pas écrire un tel code.

Comme vous peut voir dans la description du plugin ESLint ci-dessus pour la no-mutation règle :

Cette règle est tout aussi efficace que d'utiliser Object.freeze () pour empêcher les mutations dans vos réducteurs Redux. Cependant, cette règle n'a aucun coût d'exécution. Une bonne alternative à la mutation d'objet est d'utiliser la syntaxe de propagation d'objet fournie dans ES2016.


Un autre moyen relativement simple (sans utiliser de bibliothèque d'immuabilité) pour empêcher activement la mutation est de geler votre état à chaque mise à jour.

import { Map } from 'immutable';

export default function(state = Map(), action) {
  switch(action.type) {
    case SOME_ACTION:
      return state.merge({ section1: action.payload.abc });
      // this creates a new immutable state by adding the abc object into it
      // can also use mergeDeep if abc is nested
  }
  return state;
}

Voici un exemple:

const object = { a: 1, b: 2, c : 3 };
const immutable = Object.freeze(object);
object.a = 5;
console.log(immutable.a); // 1

Object.freeze est une opération superficielle, vous devrez donc geler manuellement le reste de l'objet ou utiliser une bibliothèque comme deep-freeze .

L'approche ci-dessus met la responsabilité de protéger la mutation sur vous.

Un inconvénient de cette approche est qu'il augmente l'effort cognitif nécessaire pour le programmeur en rendant explicite la protection contre les mutations et est donc plus sujet aux bugs (en particulier dans une grande base de code).

Il pourrait également y avoir une surcharge de performance lors de la congélation, en particulier si vous utilisez une bibliothèque conçue pour tester / gérer divers cas extrêmes que votre application particulière n'introduit peut-être jamais.


Une approche plus évolutive consiste à intégrer un modèle d'immuabilité dans votre logique de code, ce qui pousser les modèles de codage naturellement pour des opérations immuables.

Une approche consiste à utiliser Immutable JS où les structures de données elles-mêmes sont construites de telle sorte qu'une opération sur elles crée toujours une nouvelle instance et ne mute jamais.

export default function(state = initialState, action) {
  switch(action.type) {
    case SOME_ACTION:
      return Object.freeze({
        ...state,
        section1: Object.freeze(action.payload.abc)
      });
  }
  return state;
}

Une autre approche consiste à utiliser immer qui cache l'immuabilité dans les coulisses et vous donne des apis mutables en suivant le principe de la copie à l'écriture .

{
  ...state,
  a: {
    ...action.a
  }
}

Cette bibliothèque pourrait bien fonctionner pour vous si vous convertissez une application existante qui a probablement une grande partie du code effectuant la mutation.

L'inconvénient d'une bibliothèque d'immuabilité est qu'elle augmente la barrière à l'entrée votre base de code pour les personnes qui ne connaissent pas la bibliothèque, car maintenant tout le monde doit l'apprendre.

Cela étant dit, un modèle de codage cohérent réduit l'effort cognitif (tout le monde utilise le même modèle) et réduit le facteur de chaos du code (empêche les gens d'inventer leurs propres modèles tout le temps) en restreignant explicitement le la façon dont le code est construit. Cela entraînera naturellement moins de bogues et un développement plus rapide.


2 commentaires

Je ne peux malheureusement pas voter car je viens de créer mon compte et je n'ai pas encore activé la fonctionnalité, mais cette réponse m'a donné de nombreux outils pour rechercher et essayer et je leur en suis reconnaissant.


Heureux d'aider mon pote, pas de soucis :)