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

В этой статье мы пройдёмся по различным способам справиться с ошибками в приложениях на React.

 автор KOTELOV
автор KOTELOV

Классический метод "Try and Catch" в React

Если вы использовали JavaScript, вам, вероятно, приходилось писать инструкцию "try and catch". Чтобы убедиться в этом, посмотрите:

try {
  somethingBadMightHappen();
} catch (error) {
  console.error("Something bad happened");
  console.error(error);

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

const fetchData = async () => {
  try {
    return await fetch("https://some-url-that-might-fail.com");
  } catch (error) {
    console.error(error); // You might send an exception to your error tracker like AppSignal
    return error;
  }

При выполнении сетевых вызовов в React обычно используют инструкцию try...catch. Но почему? К сожалению, try...catch работает только с императивным кодом, но не работает с декларативным, таким как JSX, который пишут в компонентах. Вот почему вы не видите массивной упаковки  try...catch всего нашего приложения. Это просто не сработает.

Итак, что делать? В React 16 появилась новая концепция — границы ошибок React. Давайте разберемся, что это такое.

Границы ошибок React

Прежде чем мы перейдем к границам ошибок, давайте сначала посмотрим, почему они необходимы. Представьте, что у вас есть такой компонент:

const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>;
};
 
export default CrashableComponent

Если вы попытаетесь отобразить этот компонент где-нибудь, вы получите ошибку, подобную этой:

Мало того, вся страница будет пустой, и пользователь не сможет ничего делать или видеть. Но что произошло? Мы попытались получить доступ к свойству iDontExist.prop, которого не существует (мы не передаем его компоненту). Это банальный пример, но он показывает, что мы не можем поймать эти ошибки try...catch с помощью инструкции.

Весь этот эксперимент подводит нас к границам ошибок. Границы ошибок — это компоненты React, которые улавливают ошибки JavaScript в любом месте своего дочернего дерева компонентов. Затем они регистрируют эти обнаруженные ошибки и отображают резервный пользовательский интерфейс вместо дерева компонентов, которое разбилось. Границы ошибок улавливают ошибки во время рендеринга, в методах жизненного цикла и в конструкторах всего дерева под ними.

Граница ошибки — это классовый компонент, который определяет один (или оба) из методов жизненного цикла  static getDerivedStateFromError() или componentDidCatch(). static getDerivedStateFromError() отображает резервный пользовательский интерфейс после возникновения ошибки. componentDidCatch() можно передавать информацию об ошибках вашему поставщику услуг (например, AppSignal) или в консоль браузера.

Вот пример того, как информация об ошибке React выглядит в "списке проблем" AppSignal:

Давайте посмотрим на типичный компонент границы ошибки:

import { Component } from "react";
 
class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return {
      hasError: true,
      error,
    };
  }
 
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }
 
  render() {
    const { hasError, error } = this.state;
 
    if (hasError) {
      // You can render any custom fallback UI
      return (
        <div>
          <p>Something went wrong ????</p>
 
          {error.message && <span>Here's the error: {error.message}</span>}
        </div>
      );
    }
 
    return this.props.children;
  }
}
 
export default ErrorBoundary

Мы можем использовать ErrorBoundary примерно так:

<ErrorBoundary>
  <CrashableComponent />
</ErrorBoundary

Теперь, когда мы открываем наше приложение, мы получим рабочее приложение с текстом:

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

Помните, что использование границ ошибок не панацея. Границы ошибок не распознают ошибки для:

  • Обработчики событий.

  • Асинхронный код (например, setTimeout, или requestAnimationFrame Callbacks).

  • Server-side rendering.

  • Ошибки, которые возникают в самой границе ошибки (а не в ее дочерних элементах).

В этих случаях всё равно нужно использовать  try...catch. И так, давайте продолжим и покажем, как вы можете это сделать.

Перехват ошибок в обработчиках событий

Как упоминалось ранее, границы ошибок не могут нам помочь, когда ошибка выдается внутри обработчика событий. Давайте посмотрим, как можно с ними справиться. Ниже приведен небольшой компонент кнопки, который выдает ошибку при нажатии на него:

import { useState } from "react";
 
const CrashableButton = () => {
  const [error, setError] = useState(null);
 
  const handleClick = () => {
    try {
      throw Error("Oh no :(");
    } catch (error) {
      setError(error);
    }
  };
 
  if (error) {
    return <span>Caught an error.</span>;
  }
 
  return <button onClick={handleClick}>Click Me To Throw Error</button>;
};
 
export default CrashableButton

Обратите внимание, что у нас есть блок try and catch внутри handleClick, который гарантирует, что наша ошибка будет обнаружена. Если вы отобразите компонент и попытаетесь щелкнуть по нему, это произойдет:

Нужно делать то же самое в других случаях, например, в вызовах setTimeout

Перехват ошибок в вызовах setTimeout

Представьте, что у нас есть аналогичный компонент button, но он вызывает  setTimeout при нажатии. Вот как это выглядит:

import { useState } from "react";
 
const SetTimeoutButton = () => {
  const [error, setError] = useState(null);
 
  const handleClick = () => {
    setTimeout(() => {
      try {
        throw Error("Oh no, an error :(");
      } catch (error) {
        setError(error);
      }
    }, 1000);
  };
 
  if (error) {
    return <span>Caught a delayed error.</span>;
  }
 
  return (
    <button onClick={handleClick}>Click Me To Throw a Delayed Error</button>
  );
};
 
export default SetTimeoutButton

Через 1000 миллисекунд callback setTimeout  выдаст ошибку. К счастью, мы включаем эту логику обратного вызова в try...catch и в компонент setError. Таким образом, трассировка стека не отображается в консоли браузера. Кроме того, мы сообщаем об ошибке пользователю. Вот как это выглядит в приложении:

Таким образом, мы запустили страницы приложения, несмотря на то, что ошибки появляются повсюду в фоновом режиме. Но есть ли более простой способ обработки ошибок без написания пользовательских границ ошибок? Вы можете поспорить, что есть, и, конечно же, он поставляется в виде пакета JavaScript. Позвольте мне познакомить вас с react-error-boundary.

react-error-boundary пакет JavaScript

Вы можете вставить эту библиотеку в свой package.json быстрее, чем когда-либо, с:

npm install --save react-error-boundary

Теперь вы готовы использовать его. Помните компонент ErrorBoundary, который мы создали? Вы можете забыть об этом, потому что этот пакет экспортирует свои собственные. Вот как это использовать:

import { ErrorBoundary } from "react-error-boundary";
import CrashableComponent from "./CrashableComponent";
 
const FancyDependencyErrorHandling = () => {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error) => {
        // You can also log the error to an error reporting service like AppSignal
        // logErrorToMyService(error, errorInfo);
        console.error(error);
      }}
    >
      <CrashableComponent />
    </ErrorBoundary>
  );
};
 
const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong ????</p>
 
    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);
 
export default FancyDependencyErrorHandling

В этом примере визуализируем то же CrashableComponent, но на этот раз мы используем компонент ErrorBoundary из библиотеки react-error-boundary. Он делает то же самое, что и наш пользовательский, за исключением того, что он получает FallbackComponent и обработчик функции onError. Результат тот же, что и с нашим пользовательским компонентом ErrorBoundary, за исключением того, что вам не нужно беспокоиться о его обслуживании, поскольку вы используете внешний пакет.

Одна из замечательных особенностей этого пакета заключается в том, что вы можете легко обернуть свои функциональные компоненты в компонент withErrorBoundary более высокого порядка (HOC). Вот как это выглядит:

import { withErrorBoundary } from "react-error-boundary";
 
const CrashableComponent = (props) => {
  return <span>{props.iDontExist.prop}</span>;
};
 
export default withErrorBoundary(CrashableComponent, {
  FallbackComponent: () => <span>Oh no :(</span>,
});

Хорошо, теперь вы можете записывать все те ошибки, которые вас беспокоят.

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

Используя свои собственные границы React

Похожего, если не точно такого же эффекта можно достичь с помощью react-error-boundary. Мы уже разбирали кастомный ErrorBoundary компонент, но предлагаю его улучшить.

import { Component } from "react";
 
export default class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return {
      hasError: true,
      error,
    };
  }
 
  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service like AppSignal
    // logErrorToMyService(error, errorInfo);
  }
 
  render() {
    const { hasError, error } = this.state;
 
    if (hasError) {
      // You can render any custom fallback UI
      return <ErrorFallback error={error} />;
    }
 
    return this.props.children;
  }
}
 
const ErrorFallback = ({ error }) => (
  <div>
    <p>Something went wrong ????</p>
 
    {error.message && <span>Here's the error: {error.message}</span>}
  </div>
);
 
const errorBoundary = (WrappedComponent) => {
  return class extends ErrorBoundary {
    render() {
      const { hasError, error } = this.state;
 
      if (hasError) {
        // You can render any custom fallback UI
        return <ErrorFallback error={error} />;
      }
 
      return <WrappedComponent {...this.props} />;
    }
  };
};
 
export { errorBoundary };

У вас получились  ErrorBoundary и HOC errorBoundary, которые вы можете использовать во всем приложении. Их можно масштабировать и видоизменять. Вы можете сделать так, чтобы они получали индивидуальные fallback компоненты для кастомизации способов восстановления после каждой ошибки. Ещё можно настроить получение  onError, и потом вызывать его внутриcomponentDidCatch. Возможности не ограничены.

Одно могу сказать точно — эти взаимосвязи не нужны в конце концов. Уверен, написание собственного error boundary даст ощущение успеха, сможете лучше их понимать. Ну и кто знает, может, придут какие-то интересные идеи в голову, пока вы экспериментируете с кастомизацией.

Резюмируем:

  • Границы ошибок React отлично подходят для обнаружения ошибок в декларативном коде (например, внутри дерева дочерних компонентов).

  • Для других случаев необходимо использовать инструкцию  try...catch (например, асинхронные вызовы, такие как setTimeout обработчики событий, рендеринг на стороне сервера и ошибки, возникающие в самой границе ошибки).

  • Подобная библиотека react-error-boundary помогает писать меньше кода.

  • Вы также можете запустить свою собственную границу ошибок и настроить ее так, как хотите.

Комментарии (0)