React course
  • Компоненты и коллекции
  • TypeScript
  • Стилизация
  • События и состояния
  • Формы
  • Жизненный цикл
  • Функциональные vs классовые компоненты
  • Основы Redux
  • Redux Toolkit
  • Асинхронный Redux
  • Селекторы
  • React Router
  • Code splitting
  • Паттерны и контекст
  • Анимация
Powered by GitBook
On this page
  • 1. Компоненты-классы
  • 2. События
  • 2.1. Счетчик
  • 2.2. Анонимные колбеки
  • 2.3. Кастомные методы
  • 2.4. Привязка контекста
  • 2.5. Дополнительные материалы
  • 3. Внутреннее состояние компонента
  • 3.1. Начальное состояние от props
  • 3.2. Изменение состояния компонента
  • 3.3. Как обновляется состояние
  • 3.4. Асинхронность обновления состояния
  • 3.5. setState с функцией
  • 3.6. Подъем состояния (state hoisting)
  • 4. Типы внутренних данных компонента

Was this helpful?

События и состояния

1. Компоненты-классы

Если необходимо добавить динамику, компоненты создаются как классы.

  • Обычный ES6 класс, поэтому применяются все правила: конструктор, методы, контекст (this).

  • Обязательно расширяет базовый класс React.Component.

  • Действует как функция, которая получает props, но также реализует приватное внутреннее состояние.

  • Необходимо объявить обязательный метод render(), который вызывается по умолчанию и возвращает JSX-разметку.

  • Каждый раз при использовании компонента-класса, React будет создавать экземпляр компонента (класса), поэтому доступ к пропсам происходит через this.props.

  • Можно определить кастомные методы класса и использовать их в любом месте, в том числе внутри JSX, вызывать или передавать детям как пропсы.

  • Когда изменяется состояние или пропcы компонента, происходит его ре-рендер.

// Отделяйте именованные импорты, это повышает читаемость кода
import React, { Component } from 'react';

class MyClassComponent extends Component {
  static defaultProps = {};

  static propTypes = {};

  render() {
    return <div>Class Component</div>;
  }
}

2. События

Для нативного события браузера в React создается объект-обертка SyntheticEvent Object с идентичным интерфейсом. Это необходимо чтобы предоставить кросс-бразуерность и оптимизировать производительность.

<button onClick={event => console.log(event)}>Click me!</button>
  • Добавление обработчика событий с EventTarget.addEventListener() почти не используется, за редким исключением.

  • Пропсы событий не исключение и именуются с помощью camelCase. Например onClick, onChange, onSubmit, onMouseEnter.

  • В проп события передается ссылка на callback-функцию, которая будет вызвана при наступлении события.

  • Обработчики событий получают экземпляр SyntheticEvent Object.

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

Именно поэтому объект SyntheticEvent всего один (на все приложение) и доступен только в синхронном коде. Сразу после вызова callback-функции он будет использован повторно и все свойства будут аннулированы.

2.1. Счетчик

Создадим компонент-счетчик с возможностью увеличения и уменьшения значения.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

class Counter extends Component {
  static defaultProps = {
    step: 1,
  };

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button type="button">Increment by {step}</button>
        <button type="button">Decrement by {step}</button>
      </div>
    );
  }
}

ReactDOM.render(<Counter step={5} />, document.getElementById('root'));

2.2. Анонимные колбеки

Инлайн колбеки считаются антипаттерном. Каждый раз когда компонент ре-рендерится, будет создана новая callback-функция. В многих случаях это нормально. Но, если callback передается как проп нижележащим компонентам в дереве, они будут отрендерены заново, так как придут новые пропы ссылочного типа (функция).

class Counter extends Component {
  /* ... */

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button
          type="button"
          onClick={evt => {
            console.log('Increment button was clicked!', evt); // работает
            console.log('this.props: ', this.props); // работает
          }}
        >
          > Increment by {step}
        </button>
        <button
          type="button"
          onClick={evt => {
            console.log('Decrement button was clicked!', evt); // работает
            console.log('this.props: ', this.props); // работает
          }}
        >
          Decrement by {step}
        </button>
      </div>
    );
  }
}

2.3. Кастомные методы

Чаще всего обработчики событий объявляются как методы класса, после чего jsx-атрибуту передается ссылка на метод.

class Counter extends Component {
  /* ... */

  handleIncrement(evt) {
    console.log('Increment button was clicked!', evt); // работает
    console.log('this.props: ', this.props); // Error: cannot read props of undefined
  }

  handleDecrement(evt) {
    console.log('Decrement button was clicked!', evt); // работает
    console.log('this.props: ', this.props); // Error: cannot read props of undefined
  }

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button type="button" onClick={this.handleIncrement}>
          Increment by {step}
        </button>
        <button type="button" onClick={this.handleDecrement}>
          Decrement by {step}
        </button>
      </div>
    );
  }
}

2.4. Привязка контекста

Нужно всегда помнить о значении this в методах использующихся как callback-функции. В JavaScript, контекст в методах класса не привязывается по умолчанию. Если забыть привязать контекст, и передать метод как callback-функцию обработчику события, во время вызова функции, this будет неопределен (undefined).

2.4.1. Привязка при передаче колбека

Избегайте привязки контекста в методе render(). Всякий раз, когда компонент ре-рендерится, Function.prototype.bind() возвращает новую функцию и передает ее вниз по дереву компонентов, что приводит к повторному рендеру дочерних компонентов. При достаточном количестве, это оказывает существенное влияние на производительность.

// ❌ Плохо
class Counter extends Component {
  /* ... */

  handleIncrement(evt) {
    // ...
  }

  handleDecrement(evt) {
    // ...
  }

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button type="button" onClick={this.handleIncrement.bind(this)}>
          Increment by {step}
        </button>
        <button type="button" onClick={this.handleDecrement.bind(this)}>
          Decrement by {step}
        </button>
      </div>
    );
  }
}

2.4.2. Привязка в конструкторе

Еще один способ привязать контекст - сделать это в конструкторе класса. Если callback-функций много, можете себе представить, насколько большой может получиться конструктор.

  • Конструктор выполняется один раз, поэтому bind вызовется один раз

  • Методы класса записываеются в свойство prototype функции-конструктора

// ✅ Хорошо
class Counter extends Component {
  /* ... */

  constructor() {
    super();

    this.handleIncrement = this.handleIncrement.bind(this);
    this.handleDecrement = this.handleDecrement.bind(this);
  }

  handleIncrement(evt) {
    // ...
  }

  handleDecrement(evt) {
    // ...
  }

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button type="button" onClick={this.handleIncrement}>
          Increment by {step}
        </button>
        <button type="button" onClick={this.handleDecrement}>
          Decrement by {step}
        </button>
      </div>
    );
  }
}

2.4.3. Публичные свойства класса

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

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

// ✅ Хорошо
class Counter extends Component {
  /* ... */

  handleIncrement = evt => {
    console.log('Increment button was clicked!', evt); // работает
    console.log('this.props: ', this.props); // работает
  };

  handleDecrement = evt => {
    console.log('Decrement button was clicked!', evt); // работает
    console.log('this.props: ', this.props); // работает
  };

  render() {
    const { step } = this.props;

    return (
      <div>
        <span>0</span>
        <button type="button" onClick={this.handleIncrement}>
          Increment by {step}
        </button>
        <button type="button" onClick={this.handleDecrement}>
          Decrement by {step}
        </button>
      </div>
    );
  }
}

2.5. Дополнительные материалы

3. Внутреннее состояние компонента

Объект-состояния state это свойство класса которое не должно изменяться разработчиком напрямую.

  • Данные в state контролируют то, что отображается в интерфейсе.

  • Данные, хранящиеся в состоянии, должны быть информацией, которая будет обновляться методами компонента.

  • Не нужно дублировать данные из props в состоянии.

  • Каждый раз, когда изменяется состояние компонента (или пропсы), вызывается метод render().

В состоянии хранят минимально необходимый набор данных, на основе которых можно вычислить все необходимое для отрисовки интерфейса. Это делается вызовом селекторов (функций которые составляют данные для интерфейса на основе состояния) в методе render(). Так мы получаем вычисляемые данные.

  • Интерфейс зависит от состояния компонента.

  • Состояние может измениться как реакция на действия пользователя.

  • При изменении состояния, данные передаются вниз по дереву компонентов.

  • Компоненты возвращают обновленную разметку и изменяется интерфейс.

Состояние принадлежит компоненту и изменяется только его методами. Изменение состояния компонента никогда не повлияет на его родителя, соседей или любой другой компонент в приложении - только на его дочерние элементы. При такой модели, данные в приложении передаются только одним, жестко ограниченным образом. Это называется однонаправленный поток данных.

Состояние объявляется в конструкторе, так как это первое, что происходит, когда создается экземпляр класса.

class Counter extends Component {
  constructor() {
    super();

    this.state = {
      value: 0,
    };
  }

  /* ... */

  render() {
    return (
      <div>
        <span>{this.state.value}</span>
        {/* ... */}
      </div>
    );
  }
}

3.1. Начальное состояние от props

Иногда начальное состояние зависит от переданных пропсов, например начальное значение нашего счетчика. В этом случае, необходимо явно объявить параметр props в конструкторе и передать его в вызов super(props). Тогда в конструкторе будет доступно this.props.

class Counter extends Component {
  static defaultProps = {
    step: 1,
    initialValue: 0,
  };

  constructor(props) {
    super(props);

    this.state = {
      value: this.props.initialValue,
    };
  }

  /* ... */
}

ReactDOM.render(<Counter initialValue={10} />, document.getElementById('root'));

Так как под капотом используется Babel, можно пропустить утомительное объявление конструктора и указать состояние как публичное свойство класса, все остальное транспайлер сделает за нас.

class Counter extends Component {
  static defaultProps = {
    step: 1,
    initialValue: 0,
  };

  state = {
    value: this.props.initialValue,
  };

  /* ... */
}

3.2. Изменение состояния компонента

Для обновления состояния используется встроенный метод setState().

setState(updater, callback)
  • Первым, обязательным аргументом, передается объект с полями указывающими какую часть состояния необходимо изменить.

  • Вторым, необязательным аргументом, можно передать callback-функцию которая выполнится после изменения состояния.

Нельзя изменять состояние напрямую по ссылке. Будьте очень внимательны, особенно при работе со ссылочными типами (массив, объект).

state = { fullName: 'Poly' };

// ❌ Плохо
this.state.fullName = 'Mango';

// ✅ Хорошо
this.setState({
  fullName: 'Mango',
});

Этот подход используется когда новое состояние не рассчитывается на основе предыдущего. То есть когда в состояние записывается что-то новое, перезаписывая уже существующее. Сделаем компонент с переключателем, методы которого будут перезаписывать значение isOpen в состоянии.

class Toggle extends Component {
  state = { isOpen: false };

  show = () => this.setState({ isOpen: true });

  hide = () => this.setState({ isOpen: false });

  render() {
    const { isOpen } = this.state;
    const { children } = this.props;

    return (
      <>
        <button onClick={this.show}>Show</button>
        <button onClick={this.hide}>Hide</button>
        {isOpen && children}
      </>
    );
  }
}

3.3. Как обновляется состояние

При вызове setState() не нужно передавать все свойства хранящиеся в состоянии. Достаточно указать только ту часть (срез) состояния, которую мы хотим изменить в данной операции. React затем берет текущее состояние и объект, который был передан в setState(), объединяя их следующим образом.

// состояние перед объединением
const currentState = { a: 2, b: 3, c: 7, d: 9 };

// объект переданный в setState
const updateSlice = { b: 5, d: 4 };

// новое значение this.state после объединения
const nextState = { ...currentState, ...updateSlice }; // {a: 2, b: 5, c: 7, d: 4}

3.4. Асинхронность обновления состояния

Метод setState() регистрирует асинхронную операцию обновления состояния, которая ставится в очередь обновлений. React изменяет состояние не для каждого вызова setState(), а может объединять несколько вызовов в одно обновление для повышения производительности. Из-за этого доступ к this.state, в синхронном коде, после вызова этого метода вернет значение до обновления.

Представьте, что при изменении состояния, вы полагаетесь на текущее значение состояния при вычислении следующего. Используем цикл for для создания (регистрации) нескольких обновлений.

// Предположим что есть такое состояние
state = { value: 0 };

// Запустим цикл и создадим 3 операции обновления
for (let i = 0; i < 3; i += 1) {
  /*
   * Если посмотреть состояние, на всех итерациях будет 0
   * Потому что это синхронный код и обновление состояния еще не произошло
   */
  console.log(this.state.value);

  this.setState({ value: this.state.value + 1 });
}

Значение свойства this.state.value запоминается во время создания объекта передаваемого в setState(), а не во время обновления состояния. То есть, если в момент создания объекта, this.state.value содержало 0, в функцию setState() передается объект {value: 0 + 1}.

В результате выполнения цикла получаем очередь из 3-х объектов { value: 0 + 1 }, { value: 0 + 1 }, { value: 0 + 1 } и оригинальное состояние на момент обновления { value: 0 }. После всех обновлений получаем состояние { value: 1 }.

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

3.5. setState с функцией

Этот подход используется, когда новое значение вычисляется на основе предыдущего состояния. Метод setState(), первым аргументом, может принимать не объект, а функцию, которая должна возвращать объект которым мы хотим обновить состояние.

setState((state, props) => {
  return {}
}, callback)

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

// Предположим что есть такое состояние
state = { value: 0 };

// Запустим цикл и создадим 3 операции обновления
for (let i = 0; i < 3; i += 1) {
  /*
   * Если посмотреть состояние, на всех итерациях будет 0
   * Потому что это синхронный код и обновление состояния еще не произошло
   */
  console.log(this.state.value); // 0

  this.setState(prevState => {
    /*
     * Если посмотреть состояние переданное callback-функции во время ее вызова,
     * получим актуальное состояния на момент обновления.
     */
    console.log(prevState.value); // будет разный на каждой итерации

    return { value: prevState.value + 1 };
  });
}

Каждый раз, во время вызова функции переданной в setState(), в параметр prevState будет передана ссылка на актуальное состояние в момент обновления. Получим объекты обновлений {value: 0 + 1}, {value: 1 + 1}, {value: 2 + 1}, и, в результате, this.state.value будет содержать 3.

Теперь можем заменить функционал открыть/закрыть в компоненте <Toggle>.

class Toggle extends Component {
  state = { isOpen: false };

  toggle = () => {
    this.setState(state => ({ isOpen: !state.isOpen }));
  };

  render() {
    const { isOpen } = this.state;
    const { children } = this.props;

    return (
      <div>
        <button onClick={this.toggle}>{isOpen ? 'Hide' : 'Show'}</button>
        {isOpen && children}
      </div>
    );
  }
}

А счетчик будет выглядеть так.

class Counter extends Component {
  /* ... */

  handleIncrement = () => {
    this.setState((state, props) => ({
      value: state.value + props.step,
    }));
  };

  handleDecrement = () => {
    this.setState((state, props) => ({
      value: state.value - props.step,
    }));
  };

  /* ... */
}

3.6. Подъем состояния (state hoisting)

Так как React использует однонаправленный поток данных сверху вниз, для того, чтобы изменить состояние родителя при событии в ребенке, используется следующий паттерн с callback-функцией.

  • В родителе есть состояние и метод который его изменяет.

  • Ребенку, в виде пропа, пробрасывается метод родителя изменяющий состояние родителя.

  • В ребенке происходит вызов переданного ему метода.

  • При вызове этого метода изменяется состояние родителя.

  • Происходит ре-рендер поддерева компонентов родителя.

Рассмотрим простой, но наглядный пример.

/*
 * Button получает функцию changeMessage (имя пропcа),
 * которая вызывается при событии onClick
 */
const Button = ({ changeMessage, label }) => (
  <button type="button" onClick={changeMessage}>
    {label}
  </button>
);

class App extends Component {
  state = {
    message: new Date().toLocaleTimeString(),
  };

  // Метод который будем передавать в Button для вызова при клике
  updateMessage = evt => {
    console.log(evt); // Доступен объект события

    this.setState({
      message: new Date().toLocaleTimeString(),
    });
  };

  render() {
    return (
      <>
        <span>{this.state.message}</span>
        <Button label="Change message" changeMessage={this.updateMessage} />
      </>
    );
  }
}
Copy

При клике кнопки, состояние App обновляется с помощью callback-функции, контекст которой привязан к App. Этот паттерн устанавливает четкую границу между "умными" и "глупыми" компонентами.

Паттерн подъема состояния может иметь любую вложенность.

4. Типы внутренних данных компонента

  • static data - статические свойства и методы к которым необходимо получать доступ без экземпляра.

  • this.state.data - динамические данные изменяющиеся методами компонента, состояние.

  • this.data - данные которые будут разные для каждого экземпляра.

  • const DATA - константы, данные которые не изменяются и одинаковы для всех экземпляров.

PreviousСтилизацияNextФормы

Last updated 3 years ago

Was this helpful?

Документация SyntheticEvent
Pointer events with React
https://reactjs.org/docs/lifting-state-up.html