setState () dentro de componentDidUpdate ()

131

Estou escrevendo um script que move a lista suspensa abaixo ou acima da entrada, dependendo da altura da lista suspensa e da posição da entrada na tela. Também quero definir modificador para suspenso de acordo com sua direção. Mas usandosetState dentro do componentDidUpdatecria um loop infinito (o que é óbvio)

Eu encontrei uma solução ao usar getDOMNode e definir o nome da classe para o menu suspenso diretamente, mas acho que deve haver uma solução melhor usando as ferramentas React. Alguém pode me ajudar?

Aqui está uma parte do código de trabalho com getDOMNode(uma lógica de posicionamento um pouco negligenciada para simplificar o código)

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 aqui está o código com setstate (que cria um loop 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>
        );
    }
});
Katerina Pavlenko
fonte
9
Eu acho que o truque aqui é que setStatevai sempre provocar uma re-render. Em vez de verificar state.tope ligar setStatevárias vezes, basta rastrear o que você deseja state.topfazer em uma variável local e, uma vez no final da componentDidUpdatechamada, setStateapenas se sua variável local não corresponder state.top. Como está agora, você redefine imediatamente state.topapós a primeira re-renderização, o que o coloca no loop infinito.
Randy Morris
2
Veja as duas implementações diferentes de componentDidUpdateno presente violino .
Randy Morris
droga! variável local resolve todo o problema, como eu não tinha descoberto isso por mysef! Obrigado!
Katerina Pavlenko
1
Eu acho que você deve aceitar a resposta abaixo. Se você ler novamente, acho que encontrará que responde suficientemente à pergunta inicial.
Randy Morris
Por que ninguém sugeriu mudar a condição componentShouldUpdate?
Patrick Roberts

Respostas:

116

Você pode usar setStatedentro componentDidUpdate. O problema é que, de alguma forma, você está criando um loop infinito porque não há condição de interrupção.

Com base no fato de que você precisa de valores fornecidos pelo navegador depois que o componente é renderizado, acho que sua abordagem sobre o uso componentDidUpdateestá correta, ele só precisa de um tratamento melhor da condição que aciona o setState.

damianmr
fonte
4
o que você quer dizer com 'condição de quebra'? verificando se o estado já está definido e não o redefinindo?
Katerina Pavlenko
Eu concordo com isso, meu único comentário adicional seria que a adição / remoção de classes provavelmente não é necessária componentDidUpdatee pode ser adicionada conforme necessário render.
Randy Morris #
mas a adição / remoção de classe depende da posição suspensa que é registrada em componentDidUpdate, você sugere que seja verificada duas vezes? E, como eu entendo, componentDidUpdate é chamado após render (), por isso é inútil para adicionar / remover classe em render ()
Katerina Pavlenko
eu adicionei meu código com setstate, você pode verificá-lo e apontar meu erro? ou mostrar-me algum exemplo que não causaria laço
Katerina Pavlenko
2
componentDidUpdate (prevProps, prevState) {if (prevState.x == this.state.x!) {// Do Something}}
Ashok R
68

A componentDidUpdateassinatura é void::componentDidUpdate(previousProps, previousState). Com isso, você poderá testar quais objetos / estado estão sujos e chamarsetState acordo.

Exemplo:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}
Abdennour TOUMI
fonte
componentDidMountnão possui argumentos e é chamado apenas quando o componente é criado; portanto, não pode ser usado para a finalidade descrita.
Jules
@Jules Thanks! Eu costumava escrever componentDidMount, então, quando escrevi a resposta, o famoso nome caiu em cascata.
Abdennour TOUMI
componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R
Conheço sua preocupação @AshokR. Você reduz o nome do argumento. mas "prev" pode significar impedir que não seja anterior .. hhh. .kidding :)
Abdennour TOUMI
58

Se você usar setStatedentro componentDidUpdatedele, atualiza o componente, resultando em uma chamada para a componentDidUpdatequal, posteriormente, chama setStatenovamente, resultando no loop infinito. Você deve ligar condicionalmente setStatee garantir que a condição que viola a chamada ocorra eventualmente, por exemplo:

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

Caso você esteja atualizando apenas o componente enviando props para ele (ele não está sendo atualizado pelo setState, exceto pelo caso inside componentDidUpdate), você pode chamar setStateinside em componentWillReceivePropsvez de componentDidUpdate.

mickeymoon
fonte
2
pergunta antiga, mas componentWillReceiveProps está obsoleto e componentWillRecieveProps deve ser usado. Você não pode setState dentro deste método.
Brooks DuBois
Você quer dizer getDerivedStateFromProps.
adi518
5

Este exemplo ajudará você a entender os ganchos do ciclo de vida do React .

Você pode setStateno getDerivedStateFromPropsmétodo ie staticacionar o método após a troca de objetos componentDidUpdate.

Em componentDidUpdatevocê receberá o terceiro parâmetro que retorna de getSnapshotBeforeUpdate.

Você pode verificar este link 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
fonte
2

Eu diria que você precisa verificar se o estado já tem o mesmo valor que você está tentando definir. Se for o mesmo, não há motivo para definir o estado novamente para o mesmo valor.

Certifique-se de definir seu estado assim:

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

Eu tive um problema semelhante em que tenho que centralizar a dica de ferramenta. Reagir setState em componentDidUpdate me colocou em loop infinito, tentei condição funcionou. Mas descobri que o uso no retorno de chamada ref me deu uma solução mais simples e limpa; se você usar a função em linha para o retorno de chamada ref, enfrentará o problema nulo em cada atualização de componente. Portanto, use a referência de função no retorno de chamada ref e defina o estado lá, o que iniciará a nova renderização

Sanjay
fonte