100 votes

Pourquoi ne puis-je pas modifier directement l'état d'un composant, en fait ?

Je comprends que les tutoriels et la documentation sur React préviennent en en termes très clairs que l'état ne doit pas être directement muté et que tout doit passer par setState .

J'aimerais comprendre pourquoi, exactement, je ne peux pas changer directement d'état et ensuite (dans la même fonction) appeler this.setState({}) juste pour déclencher le render .

Par exemple : Le code ci-dessous semble fonctionner parfaitement :

const React = require('react');

const App = React.createClass({
  getInitialState: function() {
    return {
      some: {
        rather: {
          deeply: {
            embedded: {
              stuff: 1,
            },
          },
        },
      },
    },
  };
  updateCounter: function () {
    this.state.some.rather.deeply.embedded.stuff++;
    this.setState({}); // just to trigger the render ...
  },
  render: function() {
    return (
      <div>
        Counter value: {this.state.some.rather.deeply.embedded.stuff}
        <br></br>
        <button onClick={this.updateCounter}>Increment</button>
      </div>
    );
  },
});

export default App;

Je suis tout à fait d'accord pour suivre les conventions mais j'aimerais améliorer ma compréhension du fonctionnement de ReactJS et savoir ce qui peut mal se passer ou ce qui est sous-optimal avec le code ci-dessus.

Les notes sous le this.setState documentation identifie essentiellement deux écueils :

  1. Que si vous mutez l'état directement et que vous appelez par la suite this.setState cela peut remplacer (écraser ?) la mutation que vous avez faite. Je ne vois pas comment cela peut se produire dans le code ci-dessus.
  2. Ce setState peut muter this.state de manière asynchrone/différée, et donc lors de l'accès à l'information. this.state juste après avoir appelé this.setState vous n'avez pas la garantie d'accéder à l'état muté final. J'ai bien compris, mais ce n'est pas un problème si this.setState est le dernier appel de la fonction de mise à jour.

3 votes

Vérifiez le notes sous setState documentation . Il couvre certaines des bonnes raisons.

0 votes

@Kujira J'ai mis à jour ma question pour aborder également les notes que vous avez mentionnées.

6 votes

Outre le fait que vous pensez pouvoir le contrôler, vous ne faites que court-circuiter le flux de travail d'un cadre. Javascript vous permet de le faire, mais gardez à l'esprit qu'une fois que vous avez brisé le modèle, le framework n'est plus responsable de la cohérence de votre état.

77voto

Pranesh Ravi Points 9005

Cette réponse vise à fournir suffisamment d'informations pour ne pas changer/modifier l'état directement dans React.

React suit Flux de données unidirectionnel . En d'autres termes, le flux de données à l'intérieur de react devrait et sera censé suivre un chemin circulaire.

Flux de données de React sans flux

enter image description here

Pour faire fonctionner React de cette manière, les développeurs ont rendu React similaire à programmation fonctionnelle . La règle d'or de la programmation fonctionnelle est la suivante immuabilité . Laissez-moi vous l'expliquer haut et fort.

Comment fonctionne le flux unidirectionnel ?

  • states sont un magasin de données qui contient les données d'un composant.
  • Le site view d'un composant est rendu en fonction de l'état.
  • Lorsque le view doit changer quelque chose à l'écran, cette valeur doit être fournie par l'interface de l'utilisateur. store .
  • Pour ce faire, React fournit setState() qui prend en compte un object de nouveaux states et fait une comparaison et une fusion (similaire à object.assign() ) sur l'état précédent et ajoute le nouvel état au magasin de données d'état.
  • Chaque fois que les données dans le magasin d'état changent, react déclenchera un re-rendu avec le nouvel état que l'utilisateur doit connaître. view consomme et l'affiche à l'écran.

Ce cycle se poursuivra pendant toute la durée de vie du composant.

Si vous voyez les étapes ci-dessus, cela montre clairement que beaucoup de choses se passent derrière lorsque vous changez l'état. Ainsi, lorsque vous mutez l'état directement et que vous appelez setState() avec un objet vide. Le site previous state sera pollué par votre mutation. A cause de cela, la comparaison et la fusion superficielles de deux états seront perturbées ou ne se produiront pas, car vous n'aurez plus qu'un seul état. Cela va perturber toutes les méthodes du cycle de vie de React.

En conséquence, votre application se comportera de manière anormale, voire se plantera. La plupart du temps, cela n'affectera pas votre application car toutes les applications que nous utilisons pour les tests sont assez petites.

Et un autre inconvénient de la mutation de Objects et Arrays En JavaScript, lorsque vous assignez un objet ou un tableau, vous faites juste une référence à cet objet ou ce tableau. Lorsque vous les mutez, toutes les références à cet objet ou ce tableau seront affectées. React gère cela de manière intelligente en arrière-plan et nous donne simplement une API pour le faire fonctionner.

Les erreurs les plus courantes commises lors de la gestion des états dans React

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// bad approach
const b = this.state.a.push(6)
this.setState({
  a: b
}) 

Dans l'exemple ci-dessus, this.state.a.push(6) va muter l'état directement. En l'assignant à une autre variable et en appelant setState est identique à ce qui est montré ci-dessous. Comme nous avons muté l'état de toute façon, il n'y a pas de raison de l'assigner à une autre variable et d'appeler setState avec cette variable.

// same as 
this.state.a.push(6)
this.setState({})

Beaucoup de gens le font. C'est tellement faux . Cela brise la beauté de React et constitue une mauvaise pratique de programmation.

Alors, quelle est la meilleure façon de gérer les états dans React ? Laissez-moi vous expliquer.

Lorsque vous devez modifier "quelque chose" dans l'état existant, obtenez d'abord une copie de cette "chose" à partir de l'état actuel.

// original state
this.state = {
  a: [1,2,3,4,5]
}

// changing the state in react
// need to add '6' in the array

// create a copy of this.state.a
// you can use ES6's destructuring or loadash's _.clone()
const currentStateCopy = [...this.state.a]

Maintenant, la mutation currentStateCopy ne mutera pas l'état original. Effectuer des opérations sur currentStateCopy et le définir comme le nouvel état en utilisant setState() .

currentStateCopy.push(6)
this.setState({
  a: currentStateCopy
})

C'est magnifique, non ?

En faisant cela, toutes les références de this.state.a ne sera pas affecté jusqu'à ce que nous utilisions setState . Cela vous donne le contrôle de votre code et cela vous aidera à écrire des tests élégants et vous donnera confiance dans les performances du code en production.

Pour répondre à votre question,

Pourquoi ne puis-je pas modifier directement l'état d'un composant ?


Oui, vous pouvez . Mais vous devez faire face aux conséquences suivantes.

  1. Lorsque vous passez à l'échelle, vous écrivez du code ingérable.
  2. Vous allez perdre le contrôle de state à travers les composants.
  3. Au lieu d'utiliser React, vous écrirez des codes personnalisés sur React.

L'immuabilité n'est pas une chose nécessaire car JavaScript est monofilaire. Mais, c'est une bonne pratique à suivre qui vous aidera à long terme.

PS. J'ai écrit environ 10000 lignes de code React JS mutable. Si ça casse maintenant, je ne sais pas où chercher car toutes les valeurs sont mutées quelque part. Quand j'ai réalisé cela, j'ai commencé à écrire du code immuable. Faites-moi confiance ! C'est la meilleure chose que vous puissiez faire pour un produit ou une application.

J'espère que cela vous aidera !

3 votes

Juste un rappel : la plupart des méthodes de base pour le clonage en JS ( slice destructuration ES6, etc.) sont superficiels. Si vous avez un tableau imbriqué ou des objets imbriqués, vous devrez vous tourner vers d'autres méthodes de copie profonde, par ex. JSON.parse(JSON.stringify(obj)) (bien que cette méthode particulière ne fonctionne pas si votre objet a des références circulaires).

3 votes

@Galen Long serait _.cloneDeep suffisent

3 votes

@JoshBroadhurst Bon point, oui c'est vrai. Et si quelqu'un s'inquiète d'installer l'ensemble du paquet Lodash pour une seule fonction, vous pouvez installer uniquement le module cloneDeep en utilisant la commande suivante npm install lodash.clonedeep et l'utiliser avec let cloneDeep = require("lodash.clonedeep"); .

61voto

Ouroborus Points 6000

La documentation de React pour setState ont ceci à dire :

JAMAIS muter this.state directement, comme l'appel setState() après peut remplacer la mutation que vous avez faite. Traiter this.state comme si elle était immuable.

setState() ne mute pas immédiatement this.state mais crée une transition d'état en attente. Accès à this.state après avoir appelé cette méthode peut potentiellement retourner la valeur existante.

Il n'y a aucune garantie d'opération synchrone des appels à setState et les appels peuvent être regroupés pour améliorer les performances.

setState() déclenchera toujours un nouveau rendu, à moins qu'une logique de rendu conditionnel ne soit mise en œuvre dans l'application shouldComponentUpdate() . Si des objets mutables sont utilisés et que la logique ne peut pas être mise en œuvre dans le cadre d'un système de gestion de l'information. shouldComponentUpdate() en appelant setState() uniquement lorsque le nouvel état diffère de l'état précédent, ce qui évitera les réexpositions inutiles.

En gros, si vous modifiez this.state directement, vous créez une situation où ces modifications peuvent être écrasées.

En rapport avec vos questions étendues 1) et 2), setState() n'est pas immédiat. Il met en file d'attente une transition d'état basée sur ce qu'il pense être en train de se passer, ce qui peut ne pas inclure les modifications directes apportées à this.state . Étant donné qu'elle est mise en file d'attente plutôt qu'appliquée immédiatement, il est tout à fait possible que quelque chose soit modifié entre-temps et que vos modifications directes soient écrasées.

Si rien d'autre, vous feriez mieux de considérer que ne pas modifier directement this.state peut être considérée comme une bonne pratique. Vous savez peut-être personnellement que votre code interagit avec React d'une manière telle que ces écrasements ou autres problèmes ne peuvent pas se produire, mais vous créez une situation où d'autres développeurs ou de futures mises à jour peuvent soudainement se retrouver avec des problèmes étranges ou subtils.

0 votes

"Il met en file d'attente une transition d'état basée sur ce qu'il pense qu'il se passe, ce qui peut ne pas inclure les changements directs à cet état. Puisqu'elle est mise en file d'attente plutôt qu'appliquée immédiatement, il est tout à fait possible que quelque chose soit modifié entre temps de telle sorte que vos changements directs soient écrasés." Pourriez-vous préciser ce que vous entendez par là ? Donner un exemple où cela pose un problème ?

3 votes

@jhegedus Plongez dans la base de code de React : Gérer les changements d'état explique en détail comment le setState() et les systèmes de rappel fonctionnent et vous donnent une bonne idée de la raison pour laquelle il y aurait un problème.

0 votes

Merci pour le conseil Ouroborus !

7voto

fasil Points 102

La réponse la plus simple à la question "

Pourquoi je ne peux pas modifier directement l'état d'un composant :

c'est Phase de mise à jour.

lorsque nous mettons à jour l'état d'un composant, tous ses enfants seront également rendus. ou notre arbre de composants entier rendu.

mais quand je dis que l'ensemble de notre arbre de composants est rendu, cela ne signifie pas que le DOM entier est mis à jour. lorsqu'un composant est rendu, nous obtenons essentiellement un élément react, ce qui met à jour notre DOM virtuel.

React va alors regarder le DOM virtuel, il a aussi une copie de l'ancien DOM virtuel, c'est pourquoi nous ne devrions pas mettre à jour l'état directement Nous pouvons donc avoir deux références d'objets différentes en mémoire, l'ancien DOM virtuel et le nouveau DOM virtuel.

Ensuite, react va déterminer ce qui a été changé et sur cette base, il va mettre à jour le DOM réel en conséquence.

J'espère que cela vous aidera.

1voto

Alex Points 2064

Pour éviter de devoir créer à chaque fois une copie de this.state.element vous pouvez utiliser mise à jour avec $set or $push ou beaucoup d'autres de aide à l'immuabilité

Par exemple :

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}
});

1voto

Johan Wentholt Points 2137

Je m'étonne qu'aucune des réponses actuelles ne parle de composants purs/mémo. Ces composants ne refont le rendu que lorsqu'un changement dans l'un des props est détecté.

Supposons que vous mutiez directement l'état et que vous transmettiez, non pas la valeur, mais l'objet de surcouplage au composant situé en dessous. Cet objet a toujours la même référence que l'objet précédent, ce qui signifie que les composants purs/memo ne seront pas rendus à nouveau, même si vous avez muté l'une des propriétés.

Étant donné que l'on ne sait pas toujours avec quel type de composant on travaille lorsqu'on l'importe d'une bibliothèque, c'est une raison supplémentaire de s'en tenir à la règle de non-mutation.

Voici un exemple de ce comportement en action (à l'aide de R.evolve pour simplifier la création d'une copie et la mise à jour du contenu imbriqué) :

class App extends React.Component {
  state = { some: { rather: { deeply: { nested: { stuff: 1 } } } } };

  mutatingIncrement = () => {
    this.state.some.rather.deeply.nested.stuff++;
    this.setState({});
  }
  nonMutatingIncrement = () => {
    this.setState(R.evolve(
      { some: { rather: { deeply: { nested: { stuff: n => n + 1 } } } } }
    ));
  }

  render() {
    return (
      <div>
        Pure Component: <PureCounterDisplay {...this.state} />
        <br />
        Normal Component: <CounterDisplay {...this.state} />
        <br />
        <button onClick={this.mutatingIncrement}>mutating increment</button>
        <button onClick={this.nonMutatingIncrement}>non-mutating increment</button>
      </div>
    );
  }
}

const CounterDisplay = (props) => (
  <React.Fragment>
    Counter value: {props.some.rather.deeply.nested.stuff}
  </React.Fragment>
);
const PureCounterDisplay = React.memo(CounterDisplay);

ReactDOM.render(<App />, document.querySelector("#root"));

<script src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/ramda@0/dist/ramda.min.js"></script>
<div id="root"></div>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X