setState () all'interno di componentDidUpdate ()


131

Sto scrivendo uno script che sposta il menu a discesa sotto o sopra l'input a seconda dell'altezza del menu a discesa e della posizione dell'input sullo schermo. Inoltre voglio impostare il modificatore sul menu a discesa in base alla sua direzione. Ma l'uso setStateall'interno di componentDidUpdatecrea un ciclo infinito (che è ovvio)

Ho trovato una soluzione nell'uso getDOMNodee nell'impostazione del nome classe per il menu a discesa direttamente, ma sento che dovrebbe esserci una soluzione migliore utilizzando gli strumenti React. Qualcuno può aiutarmi?

Ecco una parte del codice di lavoro con getDOMNode(tra cui una logica di posizionamento un po 'trascurata per semplificare il codice)

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

e qui c'è il codice con setstate (che crea un ciclo infinito)

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

9
Credo che il trucco è che setStatesarà sempre innescare una ri-renderizzare. Invece di controllare state.tope chiamare setStatepiù volte, basta tenere traccia di ciò che si desidera state.topessere in una variabile locale, quindi una volta alla fine della componentDidUpdatechiamata setStatesolo se la variabile locale non corrisponde state.top. Allo stato attuale, si reimposta immediatamente state.topdopo il primo rendering, che ti mette nel ciclo infinito.
Randy Morris,

2
Vedi le due diverse implementazioni di componentDidUpdatein questo violino .
Randy Morris,

accidenti! la variabile locale risolve l'intero problema, come non l'avevo capito da mysef! Grazie!
Katerina Pavlenko,

1
Penso che dovresti accettare la risposta qui sotto. Se la rileggi, penso che troverai che risponde sufficientemente alla domanda iniziale.
Randy Morris,

Perché nessuno ha suggerito di spostare la condizione in componentShouldUpdate?
Patrick Roberts,

Risposte:


116

Puoi usare setStatedentro componentDidUpdate. Il problema è che in qualche modo stai creando un ciclo infinito perché non c'è alcuna condizione di interruzione.

Sulla base del fatto che hai bisogno di valori che vengono forniti dal browser una volta eseguito il rendering del componente, penso che il tuo approccio all'utilizzo componentDidUpdatesia corretto, ha solo bisogno di una migliore gestione della condizione che attiva il setState.


4
cosa intendi per "condizione di rottura"? verificare se lo stato è già impostato e non ripristinarlo?
Katerina Pavlenko,

Sono d'accordo con questo, il mio unico commento aggiuntivo sarebbe che l'aggiunta / rimozione di classi è probabilmente non necessaria componentDidUpdatee può essere semplicemente aggiunta secondo necessità render.
Randy Morris,

ma l'aggiunta / rimozione della classe dipende dalla posizione del menu a discesa che è archiviata in componentDidUpdate, suggerisci di controllarla due volte? E a quanto ho capito, componentDidUpdate si chiama AFTER render (), quindi è inutile aggiungere / rimuovere la classe in render ()
Katerina Pavlenko,

ho aggiunto il mio codice con setstate, puoi controllarlo e indicarmi l'errore? o mostrami qualche esempio che non causerebbe loop
Katerina Pavlenko,

2
componentDidUpdate (prevProps, prevState) {if (prevState.x! == this.state.x) {// Do Something}}
Ashok R

69

La componentDidUpdatefirma è void::componentDidUpdate(previousProps, previousState). Con questo sarai in grado di testare quali oggetti di scena / stato sono sporchi e chiamaresetState conseguenza.

Esempio:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}

componentDidMountnon ha argomenti e viene chiamato solo quando viene creato il componente, quindi non può essere utilizzato per lo scopo descritto.
Jules il

@Jules Grazie! Ho usato per scrivere componentDidMount, in modo da quando ho scritto la risposta il nome famoso cascata 😮 Ancora una volta, grazie & Great recuperare il ritardo!
Abdennour TOUMI

componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R,

Conosco la tua preoccupazione @AshokR. Riduci il nome arg. ma "prev" può significare prevenire non precedenti .. hhh. .kidding :)
Abdennour TOUMI

58

Se lo usi setStateal componentDidUpdatesuo interno aggiorna il componente, risultando in una chiamata a componentDidUpdatecui successivamente chiama di setStatenuovo risultando nel ciclo infinito. È necessario chiamare in setStatemodo condizionale e assicurarsi che alla fine si verifichi la condizione che viola la chiamata, ad esempio:

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

Nel caso in cui si stia aggiornando il componente solo inviando oggetti di scena (non viene aggiornato da setState, ad eccezione del caso all'interno di componentDidUpdate), è possibile chiamare al suo setStateinterno componentWillReceivePropsanziché componentDidUpdate.


2
vecchia domanda ma componentWillReceiveProps è deprecato e componentWillRecieveProps dovrebbe essere usato. Non è possibile impostareState all'interno di questo metodo.
Brooks DuBois,

Intendi getDerivedStateFromProps.
adi518,

5

Questo esempio ti aiuterà a comprendere i ganci del ciclo di vita di React .

È possibile setStatenel getDerivedStateFromPropsmetodo ie staticattivare il metodo dopo il cambio di oggetti di scena componentDidUpdate.

In componentDidUpdateotterrai il 3o parametro da cui ritorna getSnapshotBeforeUpdate.

Puoi controllare questo link di 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>


2

Direi che è necessario verificare se lo stato ha già lo stesso valore che si sta tentando di impostare. Se è lo stesso, non ha senso impostare nuovamente lo stato per lo stesso valore.

Assicurati di impostare il tuo stato in questo modo:

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

-1

Ho avuto un problema simile in cui devo centrare la descrizione comandi. React setState in componentDidUpdate mi ha messo in un ciclo infinito, ho provato a condizione che funzionasse. Ma ho scoperto che l'utilizzo di ref callback mi ha dato una soluzione più semplice e pulita, se si utilizza la funzione inline per il callback ref, si dovrà affrontare il problema null per ogni aggiornamento del componente. Quindi usa il riferimento alla funzione nel callback ref e imposta lo stato lì, che avvierà il re-rendering

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.