setState () wewnątrz componentDidUpdate ()

131

Piszę skrypt, który przesuwa się poniżej lub powyżej wejścia w zależności od wysokości rozwijanego menu i pozycji wejścia na ekranie. Chcę też ustawić modyfikator na rozwijany zgodnie z jego kierunkiem. Ale użycie setStatewewnątrz componentDidUpdatetworzy nieskończoną pętlę (co jest oczywiste)

Znalazłem rozwiązanie w używaniu getDOMNodei ustawianiu nazwy klasy bezpośrednio na liście rozwijanej, ale uważam, że powinno być lepsze rozwiązanie przy użyciu narzędzi React. Czy ktoś może mi pomóc?

Oto część działającego kodu z getDOMNode(m.in. trochę zaniedbaną logiką pozycjonowania w celu uproszczenia kodu)

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

a oto kod z setstate (który tworzy nieskończoną pętlę)

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
źródło
9
Myślę, że sztuczka polega na tym, setStateże zawsze spowoduje to ponowne renderowanie. Zamiast sprawdzać state.topi wywoływać setStatewiele razy, po prostu śledź, co chcesz state.topznaleźć w zmiennej lokalnej, a następnie raz na końcu componentDidUpdatewywołania setStatetylko wtedy, gdy zmienna lokalna nie pasuje state.top. W tej chwili resetujesz się natychmiast state.toppo pierwszym ponownym renderowaniu, co umieszcza cię w nieskończonej pętli.
Randy Morris
2
Zobacz dwa różne implementacje componentDidUpdatew tym skrzypce .
Randy Morris
cholera! zmienna lokalna rozwiązuje cały problem, jak bym tego nie rozgryzł samemu! Dziękuję Ci!
Katerina Pavlenko
1
Myślę, że powinieneś przyjąć odpowiedź poniżej. Jeśli przeczytasz go ponownie, myślę, że przekonasz się, że wystarczająco odpowiada na początkowe pytanie.
Randy Morris
Dlaczego nikt nie zasugerował przeniesienia stanu do componentShouldUpdate?
Patrick Roberts,

Odpowiedzi:

116

Możesz użyć w setStateśrodku componentDidUpdate. Problem polega na tym, że w jakiś sposób tworzysz nieskończoną pętlę, ponieważ nie ma warunku przerwania.

Biorąc pod uwagę fakt, że potrzebujesz wartości, które są dostarczane przez przeglądarkę po wyrenderowaniu komponentu, myślę, że twoje podejście do używania componentDidUpdatejest poprawne, wymaga tylko lepszej obsługi warunku, który wyzwala setState.

damianmr
źródło
4
co rozumiesz przez „stan zerwania”? sprawdzanie czy stan jest już ustawiony i nie resetowanie go?
Katerina Pavlenko
Zgadzam się z tym, mój jedyny dodatkowy komentarz byłby taki, że dodawanie / usuwanie klas jest prawdopodobnie niepotrzebne componentDidUpdatei renderzamiast tego można je po prostu dodać w razie potrzeby .
Randy Morris
ale dodawanie / usuwanie klas zależy od pozycji listy rozwijanej, która jest zaznaczona w komponencie componentDidUpdate, sugerujesz sprawdzić to dwa razy? I jak rozumiem, componentDidUpdate nazywa się AFTER render (), więc dodawanie / usuwanie klasy w render () nie ma sensu
Katerina Pavlenko
dodałem swój kod w setstate, czy możesz go sprawdzić i wskazać mój błąd? albo pokaż mi przykład, który nie powodowałby pętli
Katerina Pavlenko
2
componentDidUpdate (prevProps, prevState) {if (prevState.x! == this.state.x) {// Zrób coś}}
Ashok R
68

componentDidUpdatePodpis jest void::componentDidUpdate(previousProps, previousState). Dzięki temu będziesz mógł przetestować, które właściwości / stan są brudne i setStateodpowiednio wywołać .

Przykład:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}
Abdennour TOUMI
źródło
componentDidMountnie ma żadnych argumentów i jest wywoływana tylko podczas tworzenia komponentu, więc nie może być używana w opisanym celu.
Jules,
@Jules Thanks! Kiedyś pisałem componentDidMount, więc kiedy pisałem odpowiedź, słynne imię układało się kaskadowo 😮 Jeszcze raz, dzięki i świetne nadrabianie zaległości!
Abdennour TOUMI
componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R
Wiem, że martwisz się @AshokR. Zmniejszasz nazwę arg. ale „prev” może oznaczać zapobieganie nie poprzednie .. hhh. .kidding :)
Abdennour TOUMI
58

Jeśli użyjesz setStatewewnątrz componentDidUpdate, aktualizuje komponent, powodując wywołanie, componentDidUpdatektóre następnie wywołuje setStateponownie, powodując nieskończoną pętlę. Powinieneś warunkowo zadzwonić setStatei upewnić się, że stan naruszający wezwanie wystąpi w końcu, np .:

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

W przypadku, gdy aktualizujesz komponent tylko przez wysłanie do niego właściwości (nie jest aktualizowany przez setState, z wyjątkiem przypadku wewnątrz komponentu componentDidUpdate), możesz wywołać setStateinside componentWillReceivePropszamiast componentDidUpdate.

mickeymoon
źródło
2
stare pytanie, ale składnik componentWillReceiveProps jest przestarzały i należy użyć elementu componentWillRecieveProps. Nie możesz ustawićState wewnątrz tej metody.
Brooks DuBois
Masz na myśli getDerivedStateFromProps.
adi518
5

Ten przykład pomoże ci zrozumieć hooki cyklu życia reakcji .

Możesz setStatew getDerivedStateFromPropsmetodzie ie staticwywołać metodę po zmianie właściwości w componentDidUpdate.

W componentDidUpdatedostaniesz trzeci parametr, który wraca z getSnapshotBeforeUpdate.

Możesz sprawdzić ten kod link do skrzynki

// 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
źródło
2

Powiedziałbym, że musisz sprawdzić, czy stan ma już tę samą wartość, którą próbujesz ustawić. Jeśli jest taki sam, nie ma sensu ponownie ustawiać stanu dla tej samej wartości.

Upewnij się, że ustawiłeś swój stan w ten sposób:

let top = newValue /*true or false*/
if(top !== this.state.top){
    this.setState({top});
}
gradosevic
źródło
-1

Miałem podobny problem, w którym muszę wyśrodkować etykietkę narzędzia. Reakcja setState w komponencie componentDidUpdate umieściła mnie w nieskończonej pętli, próbowałem pod warunkiem, że zadziałało. Ale odkryłem, że użycie funkcji zwrotnej ref dało mi prostsze i czystsze rozwiązanie, jeśli użyjesz funkcji inline do wywołania zwrotnego ref, napotkasz problem zerowy dla każdej aktualizacji komponentu. Więc użyj odwołania do funkcji w wywołaniu zwrotnym ref i ustaw tam stan, który zainicjuje ponowne renderowanie

Sanjay
źródło