Запобіжники

Раніше помилки JavaScript всередині компонентів призводили до пошкодження внутрішнього стану бібліотеки React та спричиняли видачу незрозумілих помилок під час наступних рендерів. Ці помилки були завжди спричинені попереднью помилкою в коді програми. React не надавав можливості вчасно їх опрацювати в компонентах та не міг відновитися після них.

Представляємо запобіжники

Помилка JavaScript в деякій частині UI не повинна ламати весь додаток. Для вирішення цієї проблеми React версії 16 вводить для користувачів новий концепт – «запобіжник» (error boundary).

Запобіжники – це React-компоненти, які відслідковують помилки JavaScript в усьому дереві своїх дочірніх компонентів, логують їх, а також відображають запасний UI замість дерева компонентів, що зламалось. Запобіжники можуть ловити помилки під час рендеру, в методах життєвого циклу та в конструкторах компонентів, що знаходяться в дереві під ними.

Примітка

Запобіжники не можуть піймати помилки в:

  • обробниках подій (дізнатися більше)
  • асинхронному коді (наприклад, функції зворотнього виклику, передані в setTimeout чи requestAnimationFrame)
  • серверному рендерингу (Server-side rendering)
  • самому запобіжнику (не в його дочірніх компонентах)

Класовий компонент стане запобіжником, якщо він визначить один (або обидва) методи життєвого циклу: static getDerivedStateFromError() та componentDidCatch(). Використовуйте static getDerivedStateFromError() для рендеру запасного UI, після того як відбулась помилка. Для логування помилки використовуйте componentDidCatch().

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Оновлюємо стан, щоб наступний рендер показав запасний UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // Ви також можете передати помилку в службу звітування про помилки
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // Ви можете відрендерити будь-який власний запасний UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

Потім використовуємо запобіжник як звичайний компонент:

<ErrorBoundary>
  <MyWidget />
</ErrorBoundary>

Запобіжники працюють як блок catch {} в JavaScript, тільки для компонентів. Тільки класові компоненти можуть бути запобіжниками. На практиці, в більшості випадків буде доцільно оголосити один запобіжник і потім використовувати його по всьому додатку.

Зверніть увагу, що запобіжники можуть піймати помилки лише в компонентах, що знаходяться під ними в дереві компонентів. Запобіжник не може піймати помилку в собі. Якщо він зламається при спробі відрендерити повідомлення про помилку, то помилка пошириться до наступного запобіжника вище нього в дереві компонентів. Це також схоже на те, як працює блок catch {} в JavaScript.

Демонстрація

Подивіться приклад оголошення і використання запобіжника в React 16.

Де ставити запобіжники

Вирішуйте на ваш розсуд, як часто ставити запобіжники. Було б доцільно обгорнути компоненти маршрутів найвищого рівня, щоб показати користувачеві повідомлення «Щось пішло не так», так само, як це часто робиться в фреймворках на стороні сервера. Ви також можете обгорнути в запобіжник окремі віджети, щоб захистити решту додатку від збоїв в них.

Нова поведінка неспійманих помилок

Ця зміна має важливі наслідки. Починаючи з React версії 16, помилки, які не були спіймані жодним з запобіжників, призведуть до демонтування всього дерева React-компонентів.

Ми довго обговорювали це рішення і, судячи з нашого досвіду, гірше було б залишити пошкоджений UI на місці, ніж повністю його вилучити. Наприклад, в такому продукті як чат (Facebook Messenger), відображення пошкодженого UI може призвести до того, що хтось надіслав би повідомлення не тій людині. Аналогічно, ще гірше у додатку з проведення платежів відобразити невірну суму, ніж не відобразити взагалі нічого.

Ця зміна означає, що при переході на React версії 16, ви найбільш ймовірно виявите існуючі збої у вашому додатку, які були непомічені до цього. Додавання запобіжників дозволить вам надати кращий досвід користувачам, якщо щось піде не так.

Наприклад, Facebook Messenger огортає вміст бічної панелі, інформаційної панелі, історію повідомлень та поле введення повідомлень в окремі запобіжники. Якщо якийсь компонент в одній з цих UI зон дасть збій, то решта зон залишаться працюючими.

Ми також рекомендуємо вам використовувати існуючі служби звітування про помилки JS (або створити власну), таким чином ви зможете дізнатись про необроблені виняткові ситуації, які відбулись в продакшн та виправити їх.

Стек викликів компонентів

React 16 в режимі розробки виводить в консоль всі помилки, що відбулись під час рендеру, навіть якщо додаток ненавмисно їх поглинає. Додатково до повідомлення про помилку і стека викликів JavaScript він також надає трасування стека компонентів. Тобто відтепер ви зможете побачити, де саме в дереві компонентів відбувся збій:

Помилка виявлена запобіжником

Ви також маєте змогу знайти імена файлів та номери рядків в трасуванні стека компонентів. Це працює за замовчуванням в проектах на основі Create React App:

Помилка з номерами рядків виявлена запобіжником

Якщо ви не користуєтесь Create React App, то ви можете додати вручну цей плагін для трансформації коду до вашої конфігурації Babel. Зверніть увагу, що він призначений лише для режиму розробки і повинен бути відключений в продакшн.

Примітка

Імена компонентів, що будуть відображені в трасуванні стека, залежать від властивості Function.name. У разі, якщо ви підтримуєте старіші браузери та пристрої, де ця властивість ще не реалізована (наприклад, IE 11), розгляньте можливість додавання поліфілу Function.name в бандл вашого додатку, наприклад function.name-polyfill. Або в якості альтернативи, ви можете явно задати властивість displayName для всіх ваших компонентів.

Як щодо try/catch?

try / catch – чудова конструкція, але вона працює лише в імперативному коді:

try {
  showButton();
} catch (error) {
  // ...
}

Однак React-компоненти є декларативними, вказуючи що повинно бути відрендерено:

<Button />

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

Як щодо обробників подій?

Запобіжники не ловлять помилки, що відбулись в обробниках подій.

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

Якщо вам потрібно піймати помилку всередині обробника подій, використовуйте звичайний JavaScript вираз try / catch:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // Робимо щось, що може згенерувати виняткову ситуацію
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return <h1>Сталася помилка.</h1>
    }
    return <div onClick={this.handleClick}>Натисни на мене</div>
  }
}

Зверніть увагу, що наведений вище приклад демонструє звичайну поведінку JavaScript і не використовує запобіжники.

Зміна назви методу починаючи з React версії 15

React версії 15 включав дуже обмежену підтримку запобіжників під іншою назвою – unstable_handleError. Цей метод більше не працює і вам треба замінити його в вашому коді на componentDidCatch, починаючи з першого бета-випуску React версії 16.

В звязку з цими змінами, ми надаємо codemod-скрипт для автоматизації міграції вашого коду.