Давайте признаем, что тот, кто ищет что-то в интернете, не хочет наткнуться на нерабочую пустую страницу. Это, как минимум, путает, сбивает с толку. Сидишь и не понимаешь, что произошло и почему, это оставляет плохое впечатление о сайте. Часто лучше сообщить об ошибке и дать пользователю продолжить пользоваться приложением или сайтом. В таком случае он получит меньше негативных эмоций и не закроет это приложение.
В этой статье мы пройдёмся по различным способам справиться с ошибками в приложениях на React.
Классический метод "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
помогает писать меньше кода.Вы также можете запустить свою собственную границу ошибок и настроить ее так, как хотите.