setState () внутри componentDidUpdate ()

132

Я пишу сценарий, который перемещает раскрывающийся список ниже или выше ввода в зависимости от высоты раскрывающегося списка и положения ввода на экране. Также я хочу установить модификатор для раскрывающегося списка в соответствии с его направлением. Но использование setStateвнутри componentDidUpdateсоздает бесконечный цикл (что очевидно)

Я нашел решение в использовании getDOMNodeи настройке имени класса в раскрывающемся списке напрямую, но я считаю, что должно быть лучшее решение с использованием инструментов React. Кто-нибудь может мне помочь?

Вот часть рабочего кода с getDOMNode(например, немного упущенной логикой позиционирования для упрощения кода)

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

и вот код с setstate (который создает бесконечный цикл)

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
Я думаю, что хитрость здесь в том, что всегдаsetState будет выполняться повторный рендеринг. Вместо того, чтобы проверять и вызывать несколько раз, просто отслеживайте, что вы хотите видеть в локальной переменной, а затем один раз в конце вызова, только если ваша локальная переменная не совпадает . В его нынешнем виде вы сразу же перезагружаетесь после первого повторного рендеринга, что помещает вас в бесконечный цикл. state.topsetStatestate.topcomponentDidUpdatesetStatestate.topstate.top
Рэнди Моррис
2
Посмотрите две разные реализации componentDidUpdateв этой скрипке .
Рэнди Моррис,
черт побери! локальная переменная решает всю проблему, как бы я не понял это с помощью mysef! Спасибо!
Катерина Павленко
1
Думаю, вам стоит принять ответ ниже. Если вы прочтете его снова, я думаю, вы обнаружите, что он достаточно отвечает на первоначальный вопрос.
Рэнди Моррис
Почему никто не предложил изменить это условие componentShouldUpdate?
Патрик Робертс

Ответы:

116

Можно использовать setStateвнутрь componentDidUpdate. Проблема в том, что каким-то образом вы создаете бесконечный цикл, потому что нет условия разрыва.

Основываясь на том факте, что вам нужны значения, которые предоставляются браузером после рендеринга компонента, я думаю, что ваш подход к использованию componentDidUpdateправильный, ему просто нужна лучшая обработка условия, которое запускает setState.

damianmr
источник
4
что вы подразумеваете под «условием разрыва»? проверять, установлено ли состояние, и не сбрасывать его?
Катерина Павленко
Я согласен с этим, мой единственный дополнительный комментарий будет заключаться в том, что добавление / удаление классов, вероятно, не нужно componentDidUpdateи может быть просто добавлено по мере необходимости renderвместо этого.
Рэнди Моррис
но добавление / удаление класса зависит от позиции раскрывающегося списка, которая отмечена в componentDidUpdate, вы предлагаете проверить это дважды? И как я понимаю, componentDidUpdate вызывается ПОСЛЕ render (), поэтому добавлять / удалять класс в render () бесполезно
Катерина Павленко
я добавил свой код с помощью setstate, вы можете проверить его и указать мне на мою ошибку? или покажите мне пример, который не вызывает петли
Катерина Павленко
2
componentDidUpdate (prevProps, prevState) {if (prevState.x! == this.state.x) {// Сделайте что-нибудь}}
Ashok R
70

componentDidUpdateПодпись void::componentDidUpdate(previousProps, previousState). С его помощью вы сможете проверить, какие свойства / состояния загрязнены, и setStateсоответственно вызвать .

Пример:

componentDidUpdate(previousProps, previousState) {
    if (previousProps.data !== this.props.data) {
        this.setState({/*....*/})
    }
}
Абденнур Туми
источник
componentDidMountне имеет аргументов и вызывается только при создании компонента, поэтому не может использоваться для описанной цели.
Жюль
@Jules Спасибо! Раньше я писал componentDidMount, поэтому, когда я писал ответ, известное имя каскадировалось 😮 Еще раз спасибо и отличное наверстывание!
Abdennour TOUMI
componentDidUpdate(prevProps, prevState) { if ( prevState.x!== this.state.x) { //Do Something } }
Ashok R
Я знаю вашу озабоченность @AshokR. Вы уменьшаете имя аргумента. но "пред" может означать предотвращение, а не предыдущее .. ххх. .kidding :)
Абденнур ТУМИ
59

Если вы используете setStateвнутри, componentDidUpdateон обновляет компонент, что приводит к вызову, componentDidUpdateкоторый впоследствии вызывает setStateснова, что приводит к бесконечному циклу. Вы должны условно позвонить setStateи убедиться, что в конечном итоге возникает условие, нарушающее вызов, например:

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

Если вы обновляете только компонент, отправляя ему реквизиты (он не обновляется с помощью setState, за исключением случая внутри componentDidUpdate), вы можете вызвать setStateвнутри componentWillReceivePropsвместо componentDidUpdate.

mickeymoon
источник
2
старый вопрос, но componentWillReceiveProps устарел, и следует использовать componentWillRecieveProps. Вы не можете установить состояние внутри этого метода.
Brooks DuBois
Вы имеете в виду getDerivedStateFromProps.
adi518
5

Этот пример поможет вам разобраться в хуках жизненного цикла React .

Вы можете setStateв getDerivedStateFromPropsметоде ie staticи запускать метод после изменения реквизита componentDidUpdate.

В componentDidUpdateвы получите 3 - ю из параметров , который возвращает из getSnapshotBeforeUpdate.

Вы можете проверить эту ссылку codeandbox

// 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

Я бы сказал, что вам нужно проверить, имеет ли состояние уже то же значение, которое вы пытаетесь установить. Если это то же самое, нет смысла снова устанавливать состояние для того же значения.

Обязательно установите свое состояние следующим образом:

let top = newValue /*true or false*/
if(top !== this.state.top){
    this.setState({top});
}
gradosevic
источник
-1

У меня была аналогичная проблема, когда мне приходилось центрировать подсказку. React setState в componentDidUpdate поставил меня в бесконечный цикл, я попробовал условие, которое сработало. Но я обнаружил, что использование в обратном вызове ref дало мне более простое и чистое решение, если вы используете встроенную функцию для обратного вызова ref, вы столкнетесь с нулевой проблемой при каждом обновлении компонента. Поэтому используйте ссылку на функцию в обратном вызове ref и установите там состояние, которое инициирует повторную визуализацию

Санджай
источник