ЛР № 9

Обробка подій

Обробка подій для React-елементів дуже схожа до обробки подій для DOM-елементів. Але є деякі синтаксичні відмінності:

  • Події React іменуються в camelCase, а не в нижньому регістрі.
  • З JSX ви передаєте функцію як обробник події замість рядка.

Наприклад, HTML:

<button onclick="activateLasers()">
  Активувати лазери
</button>

дещо відрізняється в React:

<button onClick={activateLasers}>  Активувати лазери
</button>

Інша відмінність полягає в тому, що ви не можете повернути false для того, щоб запобігти поведінці за замовчуванням у React. Ви маєте явно викликати preventDefault. Наприклад, для звичайного HTML, щоб запобігти поведінці посилання за замовчуванням (відкриття нової сторінки) ви можете написати:

<a href="#" onclick="console.log('Посилання було натиснуте.'); return false">
  Натисни на мене
</a>

У React це може виглядати так:

function ActionLink() {
  function handleClick(e) {    e.preventDefault();    console.log('Посилання було натиснуте.');  }
  return (
    <a href="#" onClick={handleClick}>      Натисни на мене
    </a>
  );
}

Тут e - це синтетична подія. React визначає ці синтетичні події відповідно до специфікації W3C, тому вам не потрібно турбуватися про сумісніть між браузерами. React-події працюють інакше, ніж нативні події. Перегляньте довідник по SyntheticEvent, щоб дізнатися більше.

Зазвичай, коли ви використовуєте React, вам не потрібно викликати addEventListener, щоб додати обробник до DOM-елементу після його створення. Натомість, просто вкажіть обробник, коли елемент вперше відрендерився.

Коли ви визначаєте компонент як клас ES6, поширеною практикою є оголошення обробника подій як методу цього класу. Наприклад, цей Toggle компонент рендерить кнопку, котра дозволяє користувачу перемикатись між станами “ON” і “OFF”:

class Toggle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {isToggleOn: true};

    // Ця прив'язка необхідна, щоб `this` працював у функції зворотнього виклику    this.handleClick = this.handleClick.bind(this);  }

  handleClick() {    this.setState(state => ({      isToggleOn: !state.isToggleOn    }));  }
  render() {
    return (
      <button onClick={this.handleClick}>        {this.state.isToggleOn ? 'ON' : 'OFF'}
      </button>
    );
  }
}

ReactDOM.render(
  <Toggle />,
  document.getElementById('root')
);

Ви маєте бути обережним із значенням this у JSX функціях зворотнього виклику. У JavaScript класові методи за замовчуванням не зв’язані. Якщо ви забудете зв’язати this.handleClick і передати її до onClick this буде undefined під час виклику функції.

Така поведінка не є особливою лише для React - вона є частиною того, як функції працюють в JavaScript. В загальному випадку, якщо ви посилаєтесь на метод без () після нього, як наприклад onClick={this.handleClick}, ви маєте зв’язати цей метод.

Якщо виклики bind дратують вас - є два шляхи обійти їх. Якщо ви використовуєте експериментальний синтаксис відкритих полей класу, то ви можете використовувати поля класу, щоб правильно прив’язувати функції зворотнього виклику:

class LoggingButton extends React.Component {
  // Цей синтаксис забезпечує прив'язку `this` всередині handleClick.
  // Попередження: це *експериментальний* синтаксис.  
  handleClick = () => {
    console.log('this це:', this);  
  }
  render() {
    return (
      <button onClick={this.handleClick}>
        Натисни на мене
      </button>
    );
  }
}

За замовчуванням, цей синтаксис включено до Create React App.

Якщо ви не використовуєте синтаксис полей класу, ви можете використати стрілкову функцію в функції зворотнього виклику:

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this це:', this);
  }

  render() {
    // Цей синтаксис забезпечує прив'язку `this` всередині handleClick.    
    return (
      <button onClick={() => this.handleClick()}>
        Натисни на мене
      </button>);
  }
}

Проблема цього синтаксису полягає в тому, що при кожному рендері LoggingButton створюється нова функція зворотнього виклику. У більшості випадків це не створює додаткових проблем. Але, якщо ця функція зворотнього виклику передається в якості пропcу в компоненти нижче - вони можуть здійснити додатковий ререндеринг. Як правило, ми рекомендуємо зв’язувати в конструкторі або використувати синтаксис полей класу, щоб уникнути подібних проблем з продуктивністю.

Передача аргументів до обробників подій

Всередині циклу доволі часто потрібно передати додатковий параметр до обробника події. Наприклад, якщо id це ID рядка, можна застосувати один з нижченаведених прикладів:

<button onClick={(e) => this.deleteRow(id, e)}>Видалити рядок</button>
<button onClick={this.deleteRow.bind(this, id)}>Видалити рядок</button>

Обидва приклади є еквівалентними і використовують стрілкові функції та  Function.prototype.bind відповідно.

В обох випадках аргумент e, який відповідає події React, буде переданий другим аргументом після ID. Для стрілкової функції ми маємо зробити передачу явною, але для bind будь-які наступні аргументи будуть передані автоматично.


Ознайомлення з Хуками

Хуки — це новинка в React 16.8. Вони дозволяють вам використовувати стан та інші можливості React без написання класу.

import React, { useState } from 'react';

function Example() {
  // Створюємо нову змінну стану, яку назвемо "count"  const [count, setCount] = useState(0);
  return (
    <div>
      <p>Ви натиснули {count} разів</p>
      <button onClick={() => setCount(count + 1)}>
        Натисни мене
      </button>
    </div>
  );
}

Нова функція useState є першим “хуком”, про який ми дізнаємося більше. Цей приклад є лише тизером. Не хвилюйтеся, якщо вам поки що нічого не зрозуміло!

Почати вивчення хуків можна на наступній сторінці. На цій ми пояснимо, чому додаємо хуки до React та як вони можуть допомогти у написанні чудових застосунків.

Еквівалентний приклад з класом

Якщо ви вже використовували класи в React, цей код має бути знайомим:

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  render() {
    return (
      <div>
        <p>Ви натиснули {this.state.count} разів</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Натисни мене
        </button>
      </div>
    );
  }
}

Стан ініціалізується як { count: 0 } і ми інкрементуємо state.count, коли користувач натискає на кнопку, викликаючи this.setState(). Ми будемо використовувати фрагменти коду цього класу на цій сторінці.

Правильно використовувати стан

Є три речі, які ви повинні знати про setState().

Не змінюйте стан безпосередньо

Наприклад, це не буде повторно рендерити компонент:

// Wrong
this.state.comment = 'Привіт';

Натомість використовуйте setState():

// Correct
this.setState({comment: 'Привіт'});

Конструктор — це єдине місце, де можна присвоїти this.state.

Станові оновлення можуть бути асинхронними

React може групувати кілька викликів setState() в одне оновлення для продуктивності.

Оскільки this.props і this.state можуть бути оновлені асинхронно, не варто покладатися на їх значення для обчислення наступного стану.

Наприклад, цей код може не оновити лічильник:

// Wrong
this.setState({
  counter: this.state.counter + this.props.increment,
});

Щоб виправити це, скористайтеся другою формою setState(), яка приймає функцію, а не об’єкт. Ця функція отримає попередній стан як перший аргумент і значення пропсів безпосередньо в момент оновлення як другий аргумент:

// Correct
this.setState((state, props) => ({
  counter: state.counter + props.increment
}));

Вище було використано стрілкову функцію, але це також працює і зі звичайними функціями:

// Correct
this.setState(function(state, props) {
  return {
    counter: state.counter + props.increment
  };
});

Об’єднання оновлень стану

Коли ви викликаєте setState(), React об’єднує об’єкт, який ви надаєте, із поточним станом.

Наприклад, ваш стан може містити кілька незалежних змінних:

  constructor(props) {
    super(props);
    this.state = {
      posts: [],      comments: []    };
  }

Тоді ви можете оновлювати їх окремо за допомогою викликів setState():

  componentDidMount() {
    fetchPosts().then(response => {
      this.setState({
        posts: response.posts      });
    });

    fetchComments().then(response => {
      this.setState({
        comments: response.comments      });
    });
  }

Злиття є поверхневим, а тому this.setState({comments}) залишає this.state.posts незмінним, але повністю замінює this.state.comments.

Потік даних вниз

Ні батьківські, ні дочірні компоненти не можуть знати, чи є певний компонент становим або безстановим, і вони не повинні піклуватися, чи визначено його як функцію або клас.

Саме тому стан часто називають внутрішнім або інкапсульованим. Він не доступний для будь-якого іншого компоненту, окрім того, який ним володіє і встановлює.

Компонент може передати свій стан вниз у якості пропсів до своїх дочірніх компонентів:

<FormattedDate date={this.state.date} />

Компонент FormattedDate отримає date у своїх пропсах і не буде знати, чи він належить стану Clock, чи пропсам Clock, чи був введений вручну:

function FormattedDate(props) {
  return <h2>Зараз {props.date.toLocaleTimeString()}.</h2>;
}

Це зазвичай називається “зверху вниз” або “односпрямованим” потоком даних. Будь-який стан завжди належить певному компоненту і будь-які дані або UI, отримані з цього стану, можуть впливати лише на компоненти, що знаходяться “нижче” у дереві.


Завдання

Спираючись на результати виконання попередньої роботи розробити комонети:

  • Форму для додавання нових записів витрат, що повинна зявлятися принасканні на кнопку (дизайн навдено на малюнках).
  • Форму фільтр для відображення витрат за певний рік.
  • Компонет гістограма для відображення витрат по місяцям обраного року.

а - Кнопка відображення форми, фільтр, гістограмаб - Форма додавання записів, фільтр, гістограма