setState () à l'intérieur de componentDidUpdate ()

131

J'écris un script qui déplace la liste déroulante en dessous ou au-dessus de l'entrée en fonction de la hauteur de la liste déroulante et de la position de l'entrée sur l'écran. Je souhaite également définir le modificateur sur la liste déroulante en fonction de sa direction. Mais utiliser à l' setStateintérieur de componentDidUpdatecrée une boucle infinie (ce qui est évident)

J'ai trouvé une solution en utilisant getDOMNodeet en définissant le nom de classe directement sur la liste déroulante, mais je pense qu'il devrait y avoir une meilleure solution en utilisant les outils React. Quelqu'un peut-il m'aider?

Voici une partie du code de travail avec getDOMNode(une logique de positionnement un peu négligée pour simplifier le code)

let SearchDropdown = React.createClass({
    componentDidUpdate(params) {
        let el = this.getDOMNode();
        el.classList.remove('dropDown-top');
        if(needToMoveOnTop(el)) {
            el.top = newTopValue;
            el.right = newRightValue;
            el.classList.add('dropDown-top');
        }
    },
    render() {
        let dataFeed = this.props.dataFeed;
        return (
            <DropDown >
                {dataFeed.map((data, i) => {
                    return (<DropDownRow key={response.symbol} data={data}/>);
                })}
            </DropDown>
        );
    }
});

et voici le code avec setstate (qui crée une boucle infinie)

let SearchDropdown = React.createClass({
    getInitialState() {
        return {
            top: false
        };
    },
    componentDidUpdate(params) {
        let el = this.getDOMNode();
        if (this.state.top) {
           this.setState({top: false});
        }
        if(needToMoveOnTop(el)) {
            el.top = newTopValue;
            el.right = newRightValue;
            if (!this.state.top) {
              this.setState({top: true});
           }
        }
    },
    render() {
        let dataFeed = this.props.dataFeed;
        let class = cx({'dropDown-top' : this.state.top});
        return (
            <DropDown className={class} >
                {dataFeed.map((data, i) => {
                    return (<DropDownRow key={response.symbol} data={data}/>);
                })}
            </DropDown>
        );
    }
});
Katerina Pavlenko
la source
9
Je pense que le truc ici est que setStatecela déclenchera toujours un nouveau rendu. Plutôt que de vérifier state.topet d'appeler setStateplusieurs fois, suivez simplement ce que vous voulez state.topêtre dans une variable locale, puis une fois à la fin de l' componentDidUpdateappel setStateuniquement si votre variable locale ne correspond pas state.top. Dans l'état actuel des choses, vous réinitialisez immédiatement state.topaprès le premier rendu, ce qui vous place dans la boucle infinie.
Randy Morris
2
Voir les deux implémentations différentes de componentDidUpdatedans ce violon .
Randy Morris
bon sang! La variable locale résout tout le problème, comment ne l'avais-je pas compris par mysef! Je vous remercie!
Katerina Pavlenko
1
Je pense que vous devriez accepter la réponse ci-dessous. Si vous le relisez, je pense que vous constaterez qu'il répond suffisamment à la question initiale.
Randy Morris
Pourquoi personne n'a suggéré de déplacer la condition componentShouldUpdate?
Patrick Roberts

Réponses:

116

Vous pouvez utiliser à l' setStateintérieur componentDidUpdate. Le problème est que d'une manière ou d'une autre, vous créez une boucle infinie car il n'y a pas de condition de rupture.

Sur la base du fait que vous avez besoin de valeurs fournies par le navigateur une fois le composant rendu, je pense que votre approche de l'utilisation componentDidUpdateest correcte, elle a juste besoin d'une meilleure gestion de la condition qui déclenche le setState.

Damianmr
la source
4
qu'entendez-vous par «condition de rupture»? vérifier si l'état est déjà défini et ne pas le réinitialiser?
Katerina Pavlenko
Je suis d'accord avec cela, mon seul commentaire supplémentaire serait que l'ajout / la suppression de classes est probablement inutile componentDidUpdateet peut simplement être ajouté au besoin à la renderplace.
Randy Morris
mais l'ajout / la suppression de classe dépend de la position de la liste déroulante qui est archivée dans componentDidUpdate, vous suggérez de le vérifier deux fois? Et si je comprends bien, componentDidUpdate s'appelle AFTER render (), il est donc inutile d'ajouter / supprimer une classe dans render ()
Katerina Pavlenko
J'ai ajouté mon code avec setstate, pouvez-vous le vérifier et me signaler mon erreur? ou montrez-moi un exemple qui ne provoquerait pas de boucle
Katerina Pavlenko
2
componentDidUpdate (prevProps, prevState) {if (prevState.x! == this.state.x) {// Faire quelque chose}}
Ashok R
69

La componentDidUpdatesignature est void::componentDidUpdate(previousProps, previousState). Avec cela, vous pourrez tester quels accessoires / états sont sales et appeler en setStateconséquence.

Exemple:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}
Abdennour TOUMI
la source
componentDidMountn'a pas d'arguments et n'est appelé que lorsque le composant est créé, il ne peut donc pas être utilisé dans le but décrit.
Jules
@Jules Merci! J'avais l'habitude d'écrire componentDidMount, alors quand j'ai écrit la réponse, le célèbre nom est tombé en cascade 😮 Encore une fois, merci et grand rattrapage!
Abdennour TOUMI
componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R
Je connais votre inquiétude @AshokR. Vous réduisez le nom de l'argument. mais "prev" peut signifier empêcher pas précédent .. hhh. .kidding :)
Abdennour TOUMI
58

Si vous utilisez à l' setStateintérieur, componentDidUpdateil met à jour le composant, ce qui entraîne un appel componentDidUpdateauquel il est ensuite appelé à setStatenouveau, ce qui entraîne la boucle infinie. Vous devez appeler conditionnellement setStateet vous assurer que la condition violant l'appel se produira éventuellement, par exemple:

componentDidUpdate: function() {
    if (condition) {
        this.setState({..})
    } else {
        //do something else
    }
}

Si vous ne mettez à jour le composant qu'en lui envoyant des accessoires (il n'est pas mis à jour par setState, à l'exception du cas à l'intérieur de componentDidUpdate), vous pouvez appeler setStateinside componentWillReceivePropsau lieu de componentDidUpdate.

Mickeymoon
la source
2
ancienne question mais componentWillReceiveProps est obsolète et componentWillRecieveProps doit être utilisé. Vous ne pouvez pas définirState dans cette méthode.
Brooks DuBois
Tu veux dire getDerivedStateFromProps.
adi518
5

Cet exemple vous aidera à comprendre les hooks de cycle de vie React .

Vous pouvez setStatedans getDerivedStateFromPropsmethod ie staticet déclencher la méthode après le changement d'accessoires componentDidUpdate.

Dans componentDidUpdatevous obtiendrez le 3ème paramètre qui revient de getSnapshotBeforeUpdate.

Vous pouvez vérifier ce lien codesandbox

// Child component
class Child extends React.Component {
  // First thing called when component loaded
  constructor(props) {
    console.log("constructor");
    super(props);
    this.state = {
      value: this.props.value,
      color: "green"
    };
  }

  // static method
  // dont have access of 'this'
  // return object will update the state
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps");
    return {
      value: props.value,
      color: props.value % 2 === 0 ? "green" : "red"
    };
  }

  // skip render if return false
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate");
    // return nextState.color !== this.state.color;
    return true;
  }

  // In between before real DOM updates (pre-commit)
  // has access of 'this'
  // return object will be captured in componentDidUpdate
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate");
    return { oldValue: prevState.value };
  }

  // Calls after component updated
  // has access of previous state and props with snapshot
  // Can call methods here
  // setState inside this will cause infinite loop
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log("componentDidUpdate: ", prevProps, prevState, snapshot);
  }

  static getDerivedStateFromError(error) {
    console.log("getDerivedStateFromError");
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    console.log("componentDidCatch: ", error, info);
  }

  // After component mount
  // Good place to start AJAX call and initial state
  componentDidMount() {
    console.log("componentDidMount");
    this.makeAjaxCall();
  }

  makeAjaxCall() {
    console.log("makeAjaxCall");
  }

  onClick() {
    console.log("state: ", this.state);
  }

  render() {
    return (
      <div style={{ border: "1px solid red", padding: "0px 10px 10px 10px" }}>
        <p style={{ color: this.state.color }}>Color: {this.state.color}</p>
        <button onClick={() => this.onClick()}>{this.props.value}</button>
      </div>
    );
  }
}

// Parent component
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: 1 };

    this.tick = () => {
      this.setState({
        date: new Date(),
        value: this.state.value + 1
      });
    };
  }

  componentDidMount() {
    setTimeout(this.tick, 2000);
  }

  render() {
    return (
      <div style={{ border: "1px solid blue", padding: "0px 10px 10px 10px" }}>
        <p>Parent</p>
        <Child value={this.state.value} />
      </div>
    );
  }
}

function App() {
  return (
    <React.Fragment>
      <Parent />
    </React.Fragment>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Nikhil Mahirrao
la source
2

Je dirais que vous devez vérifier si l'état a déjà la même valeur que vous essayez de définir. Si c'est le même, il est inutile de redéfinir l'état pour la même valeur.

Assurez-vous de définir votre état comme ceci:

let top = newValue /*true or false*/
if(top !== this.state.top){
    this.setState({top});
}
gradosevic
la source
-1

J'ai eu un problème similaire où je dois centrer l'info-bulle. React setState dans componentDidUpdate m'a mis en boucle infinie, j'ai essayé la condition que cela fonctionnait. Mais j'ai trouvé que l'utilisation dans le rappel de référence m'a donné une solution plus simple et propre, si vous utilisez la fonction en ligne pour le rappel de référence, vous serez confronté au problème nul pour chaque mise à jour de composant. Utilisez donc la référence de fonction dans le rappel de référence et définissez-y l'état, ce qui lancera le nouveau rendu

Sanjay
la source