Передача функций в компоненты


Как передать обработчик события (например, onClick) компоненту?

Передавайте обработчики событий и другие функции дочерним компонентам как props:


Код
    
  <button onClick={this.handleClick}>
    

Если вам нужен доступ к родительскому компоненту в обработчике, то необходимо привязать функцию к экземпляру компонента (см. ниже).


Как привязать функцию к экземпляру компонента?

Существует несколько способов удостовериться, что функции имеют доступ к атрибутам компонента, таким как this.props и this.state, в зависимости от того, какой синтаксис и шаги сборки вы используете.


Привязка в конструкторе (ES2015)


Код
    
  class Foo extends Component {
    constructor(props) {
      super(props);
      this.handleClick = this.handleClick.bind(this);
    }
      
    handleClick() {
      console.log('Click happened');
    }
      
    render() {
      return <button onClick={this.handleClick}>Click Me</button>;
    }
  }
    

Свойства класса


Код
    
  class Foo extends Component {
    // Замечание: данный синтаксис экспериментальный и пока не стандартизирован.
    
    handleClick = () => {
      console.log('Click happened');
    }
    
    render() {
      return <button onClick={this.handleClick}>Click Me</button>;
    }
  }
    

Привязка в методе render


Код
    
  class Foo extends Component {
    handleClick() {
      console.log('Click happened');
    }
    
    render() {
      return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
    }
  }
    


Внимание!

Использование Function.prototype.bind в render создает новую функцию каждый раз при отрисовке компонента, что может влиять на производительность (см. ниже).

Стрелочная функция в методе render


Код
    
  class Foo extends Component {
    handleClick() {
      console.log('Click happened');
    }
    
    render() {
      return <button onClick={() => this.handleClick()}>Click Me</button>;
    }
  }
    


Внимание!

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


Можно ли использовать стрелочные функции в методе render?

В общем, да, это нормально, и часто это самый простой способ передать параметры в функции-коллбэки.

Но если у вас появились проблемы с производительностью, обязательно произведите оптимизацию!


Зачем вообще нужна привязка?

В JavaScript эти два фрагмента кода не эквивалентны:


Код
    
  obj.method();
    


Код
    
  var method = obj.method;
  method();
    

Привязка гарантирует, что второй фрагмент работает так же, как и первый.

Обычно в React вам нужно привязать лишь методы, которые вы передаете другим компонентам. Например, здесь <button onClick = {this.handleClick}> метод this.handleClick передается в компонент button, поэтому вам следует привязать этот метод. Однако не следует привязывать метод render или методы жизненного цикла: мы не передаем их другим компонентам.

Данный пост от Yehuda Katz подробно объясняет, что такое привязка и как функции работают в JavaScript.


Почему моя функция вызывается каждый раз, когда отрисовывается компонент?

Убедитесь, что вы не вызываете функцию при передаче ее компоненту:


Код
    
  render() {
    // Неправильно: происходит вызов handleClick вместо его передачи как ссылки!
    return <button onClick={this.handleClick()}>Click Me</button>
  }
    

Вместо этого передайте саму функцию:


Код
    
  render() {
    // Правильно: handleClick передаётся как ссылка!
    return <button onClick={this.handleClick}>Click Me</button>
  }
    


Как передать параметр в обработчик событий или коллбэк?

Вы можете использовать стрелочную функцию, чтобы обернуть обработчик события и передать параметры:


Код
    
  <button onClick={() => this.handleClick(id)} />
    

Эквивалент с .bind:


Код
    
  <button onClick={this.handleClick.bind(this, id)} />
    


Пример: передача параметров с использованием стрелочных функций


Код
    
  const A = 65 // код символа ASCII
  
  class Alphabet extends React.Component {
    constructor(props) {
      super(props);
      
      this.handleClick = this.handleClick.bind(this);
      
      this.state = {
        justClicked: null,
        letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
      };
    }
    
    handleClick(letter) {
      this.setState({ justClicked: letter });
    }
    
    render() {
      return (
        <div>
          Просто кликни: {this.state.justClicked}
          <ul>
            {this.state.letters.map(letter =>
              <li key={letter} onClick={() => this.handleClick(letter)}>
                {letter}
            </li>
            )}
          </ul>
        </div>
      )
    }
  }
    


Пример: передача параметров с использованием атрибутов data-

В качестве альтернативного подхода вы можете использовать DOM API для хранения данных, необходимых для обработчиков событий. Рассмотрите данный подход, если вам нужно оптимизировать большое количество элементов или иметь отображаемое дерево, которое использует проверки на равенство компонента React.PureComponent.


Код
    
  const A = 65 // код символа ASCII
  
  class Alphabet extends React.Component {
    constructor(props) {
      super(props);
      
      this.handleClick = this.handleClick.bind(this);
      
      this.state = {
        justClicked: null,
        letters: Array.from({length: 26}, (_, i) => String.fromCharCode(A + i))
      };
    }
  
    handleClick(e) {
      this.setState({
        justClicked: e.target.dataset.letter
      });
    }
  
    render() {
      return (
        <div>
          Просто кликни: {this.state.justClicked}
          <ul>
            {this.state.letters.map(letter =>
              <li key={letter} data-letter={letter} onClick={this.handleClick}>
                {letter}
              </li>
            )}
          </ul>
        </div>
      )
    }
  }
    


Как предотвратить вызов функции слишком быстро или много раз подряд?

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

  • throttling: производит изменения в зависимости от частоты, основанной на времени (_.throttle)

  • debouncing: публикует изменения после периода бездействия (_.debounce)

  • requestAnimationFrame throttling: производит изменения основываясь на requestAnimationFrame (raf-schd)

Посмотрите на данную визуализацию для сравнения функций throttle и debounce.


Внимание!

_.debounce, _.throttle и raf-schd предоставляют метод cancel для отмены отложенных коллбэков. Вы должны либо вызвать этот метод из componentWillUnmount, либо проверить, чтобы компонент был по-прежнему вмонтирован в дерево во время нахождения функции в отложенном состоянии.


Throttle

Throttling не позволяет вызывать функцию более одного раза в течение определенного промежутка времени. Приведенный ниже пример задерживает обработчик «click», чтобы не вызывать его чаще, чем раз в секунду.


Код
    
  import throttle from 'lodash.throttle';
  
  class LoadMoreButton extends React.Component {
    constructor(props) {
      super(props);
      this.handleClick = this.handleClick.bind(this);
      this.handleClickThrottled = throttle(this.handleClick, 1000);
    }
  
    componentWillUnmount() {
      this.handleClickThrottled.cancel();
    }
  
    render() {
      return <button onClick={this.handleClickThrottled}>Изучить больше</button>;
    }
  
    handleClick() {
      this.props.loadMore();
    }
  }
    


Debounce

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


Код
    
  import debounce from 'lodash.debounce';
  
  class Searchbox extends React.Component {
    constructor(props) {
      super(props);
      this.handleChange = this.handleChange.bind(this);
      this.emitChangeDebounced = debounce(this.emitChange, 250);
    }
  
    componentWillUnmount() {
      this.emitChangeDebounced.cancel();
    }
  
    render() {
      return (
        <input
            type="text"
            onChange={this.handleChange}
            placeholder="Search..."
            defaultValue={this.props.value}
        />
      );
    }
  
    handleChange(e) {
      // React добавляет события в пулл, поэтому мы считываем значение перед debounce.
      // Или же мы могли бы вызвать `event.persist()` и передать событие целиком.
      // Чтобы получить больше информации смотрите learn-reactjs.ru/reference/synthetic-event#event-pool
      this.emitChangeDebounced(e.target.value);
    }
  
    emitChange(value) {
      this.props.onChange(value);
    }
  }
    


requestAnimationFrame throttling

requestAnimationFrame - это способ поставить в очередь функцию, которая будет выполнена в браузере в оптимальное время для минимизации влияния на производительность отрисовки. Функция, поставленная в очередь с помощью requestAnimationFrame, сработает в следующем кадре. Браузер будет усердно работать, чтобы обеспечить 60 кадров в секунду (60 fps).

Однако, если браузер не в состоянии, он естественным образом ограничит fps. Например, если устройство имеет 30 fps, вы получите от браузера только 30 кадров в секунду.

Использование requestAnimationFrame является полезным методом, поскольку он не позволяет выполнять более 60 обновлений в секунду. Если вы делаете 100 обновлений в секунду, это создает дополнительную работу для браузера, но пользователь все равно это не заметит.


Внимание!

Использование этого метода будет захватывать в кадр только последнее опубликованное значение. Вы можете увидеть пример того, как эта оптимизация работает на MDN.


Код
    
  import rafSchedule from 'raf-schd';
  
  class ScrollListener extends React.Component {
    constructor(props) {
      super(props);
  
      this.handleScroll = this.handleScroll.bind(this);
  
      // Создаем новую функцию, чтобы запланировать обновления.
      this.scheduleUpdate = rafSchedule(
        point => this.props.onScroll(point)
      );
    }
  
    handleScroll(e) {
      // Когда мы получаем событие scroll, планируем обновление.
      // Если мы получим много событий внутри фрейма, мы опубликуем только последнее значение.
      this.scheduleUpdate({ x: e.clientX, y: e.clientY });
    }
  
    componentWillUnmount() {
      // Отменяем все ожидающие обновления, так как компонент будет демонтирован.
      this.scheduleUpdate.cancel();
    }
  
    render() {
      return (
        <div
            style={{ overflow: 'scroll' }}
          onScroll={this.handleScroll}
        >
          <img src="/my-huge-image.jpg" />
        </div>
        );
    }
  }
    


Тестирование вашего ограничения скорости

Когда тестирование вашего кода ограничения скорости работает правильно, полезно иметь возможность быстрой перемотки времени. Если вы используете jest, вы можете использовать mock timers. Если вы используете requestAnimationFrame, то raf-stab может оказаться для вас полезным инструментом для управления сменой кадров анимации.