DevGang
Авторизоваться

React setState: правильный путь 

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

TL; DR

  1. Используйте функцию this.setState, чтобы изменить состояние ваших компонентов
  2. Если вы не зависите от старого состояния, передайте методу this.setState объект с новыми значениями
  3. Если вы хотите изменить состояние в зависимости от предыдущих значений, передайте функцию методу this.setState
  4. Передаваемая функция принимает предыдущее состояние и пропсы в качестве аргументов.
  5. Если вы работаете с объектами из своего состояния, всегда копируйте их, чтобы избежать непреднамеренных изменений

Все настраивается

Состояние - это просто данные, которые вы хотите сохранить в своем компоненте. В этом руководстве я собираюсь использовать компонент на основе классов со следующей начальной настройкой:

class App extends Component {
  state = {
    counter: 0,
  };

  clickHandler = () => {};
  render() {
    return (
      <React.Fragment>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

У меня есть <div> и кнопка. Для метода класса я использую стрелочную функцию, внутри которой я вызываю this.setState, потому что я не хочу терять свой this. Если вы хотите использовать function, ваш код должен быть следующим:

class App extends Component {
  constructor(props) {
    this.clickHandler = this.clickHandler.bind(this);
  }
  state = {
    counter: 0,
  };

  clickHandler = function () {};
  render() {
    return (
      <React.Fragment>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

или, немного короче:

class App extends Component {
  constructor(props) {
    this.clickHandler = this.clickHandler.bind(this);
  }
  state = {
    counter: 0,
  };

  clickHandler() {}
  render() {
    return (
      <React.Fragment>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

В приведенном выше коде я использую метод .bind для настройки функции с привязанным к ней значением this. Но в следующих примерах я буду придерживаться синтаксиса стрелочной функции, поскольку он более точен и элегантен.

Обновление состояния: основы

Попробуем сгенерировать случайное число и показать его в нашем <div> при нажатии кнопки.

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

class App extends Component {
  state = {
    counter: 0,
  };

  clickHandler = () => {
    this.state.counter = Math.random();
  };
  render() {
    return (
      <React.Fragment>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

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

clickHandler = () => {
  this.setState({ counter: Math.random() });
};

Эта функция является функцией, унаследованной вами от класса React.Component, и является частью React API. Он принимает новое состояние вашего компонента. Здесь важно упомянуть, что вам не нужно повторять все свойства, которые есть в вашем состоянии, чтобы изменить только одно значение. Эти два объекта будут объединены функцией this.setState. Рассмотрим следующий пример:

import React, { Component } from 'react';

export default class App extends Component {
  state = {
    counter: 0,
    title: 'setState tutorial',
  };

  clickHandler = () => {
    this.setState({ counter: Math.random() });
  };
  render() {
    return (
      <React.Fragment>
        <h1>{this.state.title}</h1>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

В приведенном выше коде я показываю заголовок в теге <h1>, который я храню внутри состояния. Когда кнопка нажата, заголовок не исчезнет.

Обновление состояния в зависимости от предыдущего состояния

Все идет нормально. Но что, если мы хотим увеличить наш предыдущий счетчик при нажатии кнопки? Технически следующий код будет работать так, как задумано:

import React, { Component } from 'react';

export default class App extends Component {
  state = {
    counter: 0,
    title: 'setState tutorial',
  };

  clickHandler = () => {
    this.setState({ counter: ++this.state.counter });
  };
  render() {
    return (
      <React.Fragment>
        <h1>{this.state.title}</h1>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

Единственное, что вы могли заметить (если вы последуете по тексту, я настоятельно рекомендую вам это сделать), - это раздражающее предупреждение в консоли. На самом деле, когда мы вызываем ++this.state.counter, мы пытаемся изменить счетчик в нашем предыдущем состоянии и вернуть значение математической операции. Не так хорошо, как хотелось бы. К счастью, React умеет делать подобное. Метод this.setState также принимает функцию, которая затем должна вернуть новое состояние. Кроме того, функция принимает два аргумента: предыдущее состояние и свойства:

import React, { Component } from 'react';

export default class App extends Component {
  state = {
    counter: 0,
    title: 'setState tutorial',
  };

  clickHandler = () => {
    this.setState((prevState, prevProps) => {
      console.log(prevState); // {counter: 0, title: "setState tutorial"}
      return { counter: ++prevState.counter };
    });
  };
  render() {
    return (
      <React.Fragment>
        <h1>{this.state.title}</h1>
        <div>{this.state.counter}</div>
        <button onClick={this.clickHandler}>Click!</button>
      </React.Fragment>
    );
  }
}

Теперь он работает без каких-либо предупреждений. Давайте обсудим, что произошло.

Зачем мне использовать функцию?

Возможно, вы говорите: я могу скопировать значение счетчика и затем увеличить его. Как в примере ниже:

let oldCounter = this.state.counter;
this.setState({ counter: ++oldCounter });

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

А как насчет объектов?

Ладно, теперь все хорошо, думаете вы. Вы защищены this.setState((prevState, prevProps) => ...). Однако есть еще один подводный камень. В javascript объекты хранятся по ссылкам. Давайте быстро проверим нашу функцию clickHandler:

clickHandler = () => {
  this.setState((prevState, prevProps) => {
    console.log(prevState === this.state); // true
    return { counter: ++prevState.counter };
  });
};

Баам! Объект prevState является тем же this.state. Когда мы имеем дело с примитивами, это не проблема. Но рассмотрим следующий пример:

Counter.js
// Counter.js
import React from 'react';

const Counter = ({ counter, clickHandler }) => {
  return (
    <React.Fragment>
      <div>{counter}</div>
      <button onClick={clickHandler}>Increment the counter above!</button>
    </React.Fragment>
  );
};

export default Counter;
App.js
// App.js
import React, { Component } from 'react';
import Counter from './Counter';

export default class App extends Component {
  state = {
    counters: [0, 0],
  };

  clickHandler = index => {
    this.setState((prevState, prevProps) => {
      let counter = prevState.counters[index];

      const counters = prevState.counters;
      counters[index] = ++counter;
      return {
        counters,
      };
    });
  };
  render() {
    const counters = this.state.counters.map((counter, index) => (
      <Counter
        counter={counter}
        key={index}
        clickHandler={() => this.clickHandler(index)}
      />
    ));
    return <React.Fragment>{counters}</React.Fragment>;
  }
}

В приведенном выше примере у меня есть два счетчика и дополнительный компонент, который отображает в <div> текущий счетчик и кнопку для его увеличения. Когда вы нажимаете кнопку, вы видите, что счетчик увеличивается в два раза. Не то, что мы искали.

Итак, в чем проблема?

В этих строках:

const counters = prevState.counters;
counters[index] = ++counter;

Здесь мы работаем с тем же массивом counters, что и в состоянии out. А массивы - это объекты. Следовательно, мы увеличиваем счетчик в два раза. Чтобы исправить это, нам просто нужно скопировать этот массив:

clickHandler = index => {
  this.setState((prevState, prevProps) => {
    let counter = prevState.counters[index];

    const counters = [...prevState.counters];
    counters[index] = ++counter;
    return {
      counters,
    };
  });
};

Поэтому, работая с объектами, не забывайте копировать их перед выполнением каких-либо операций.

Вывод

  1. Используйте функцию this.setState, чтобы изменить состояние ваших компонентов
  2. Если вы не зависите от старого состояния, передайте методу this.setState объект с новыми значениями
  3. Если вы хотите изменить состояние в зависимости от предыдущих значений, передайте функцию методу this.setState
  4. Передаваемая функция принимает предыдущее состояние и пропсы в качестве аргументов.
  5. Если вы работаете с объектами из своего состояния, всегда копируйте их, чтобы избежать непреднамеренных изменений

Источник:

#JavaScript #React
Комментарии 2
Constantine Seleznyoff 20.11.2021 в 18:43

Что за ересь!!! Счетчик удваивается по одной простой причине: StrictMode. Если строгий режим включен, то в режиме разработки setState вызывается дважды специально для того, чтобы исключить использование таких колбэков, которые не являются чистой функцией! Автор статьи сам не понимает, о чем говорит!

Для пруфа: https://github.com/facebook/react/issues/12856#issuecomment-390206425

Al XXX 26.03.2022 в 08:05

А нафига вообще такое писать: { counter: ++prevState.counter } ?? Зачем изменять прошлое состояние? Что мешало написать просто { counter: prevState.counter+1 } и никаких проблем бы не было. На ровном месте себе устроил проблему.

Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу