Выявление ошибок в React с помощью Error Boundaries
Даже в самых безупречных приложениях время от времени возникают ошибки выполнения. Сеть может выйти из строя, какая-то внутренняя служба может выйти из строя, или пользователи могут предоставить вам некоторую вводную информацию, которая просто не вычисляется. Или — ну знаете — баги. Но как лучше всего обрабатывать ошибки, чтобы ваше приложение оставалось безотказным, продолжало реагировать и обеспечивать максимально удобное взаимодействие с пользователем?
Что такое error boundaries?
Содержание статьи:
Error boundaries — это способ React обрабатывать ошибки в приложении. Они позволяют вам реагировать и восстанавливаться после ошибок времени выполнения, а также предоставляют резервный пользовательский интерфейс, если это применимо.
Идея, лежащая в основе error boundaries, заключается в том, что вы можете заключить любую часть вашего приложения в специальный компонент — так называемую границу ошибки (error boundary)- и если в этой части приложения возникнет неперехваченная ошибка, она будет содержаться в этом компоненте. Затем вы можете показать ошибку, сообщить об этом в службу отчетов об ошибках и попытаться ее исправить, если это возможно.
Error boundaries были введены в React 16 и были одной из первых функций, появившихся в результате усилий команды React по переписыванию Fiber. Это единственный компонент, который вам все еще нужно написать как компонент класса (так что пока никаких хуков!), Но он определенно должен быть частью любого современного приложения React.
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
Обратите внимание, что даже несмотря на то, что вы можете создать несколько error boundaries в своем приложении, как правило, выбирают только одну на корневом уровне. При желании вы можете использовать супер-грануляцию, но мой опыт подсказывает, что часто бывает достаточно корневого уровня.
Моя первая error boundary
Error boundary — это обычный компонент класса, который реализует один (или оба) из следующих методов:
static getDerivedStateFromError(error)
Этот метод возвращает новое состояние на основе обнаруженной ошибки. Обычно вы меняете флаг состояния, который сообщает error boundary, следует ли предоставлять резервный пользовательский интерфейс.
componentDidCatch(error, errorInfo)
Этот метод вызывается всякий раз, когда возникает ошибка. Вы можете зарегистрировать ошибку (и любую дополнительную информацию) в службе отчетов об ошибках, попытаться исправить ошибку и сделать все, что вам нужно.
Чтобы показать, как это реализовано, давайте сделаем несколько шагов. Во-первых, давайте создадим компонент обычного класса.
JavaScript class ErrorBoundary extends React.Component { render() { return this.props.children; } }
12345 | class ErrorBoundary extends React.Component { render() { return this.props.children; } } |
Этот компонент почти ничего не делает — он просто отображает своих дочерних элементов. Зарегистрируем ошибку в сервисе ошибок!
JavaScript class ErrorBoundary extends React.Component { componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { return this.props.children; } }
12345678 | class ErrorBoundary extends React.Component { componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { return this.props.children; } } |
Теперь, когда у пользователей возникает ошибка, мы получаем уведомление через службу отчетов об ошибках. Мы получим саму ошибку, а также полный стек компонентов, в которых произошла ошибка. Это значительно упростит работу по исправлению ошибок в дальнейшем!
Однако мы все еще нарушаем работу приложения! Это плохо. Давайте предоставим резервный пользовательский интерфейс. Для этого нам нужно отслеживать, находимся ли мы в ошибочном состоянии — и именно здесь на помощь приходит статический метод getDerivedStateFromError!
JavaScript class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { if (this.state.hasError) { return <h1>Oops, we done goofed up</h1>; } return this.props.children; } }
123456789101112131415 | class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { if (this.state.hasError) { return <h1>Oops, we done goofed up</h1>; } return this.props.children; } } |
И теперь у нас есть базовая, но функциональная Error boundary!
Начнем использовать Error boundary
Теперь приступим к ее использованию. Просто оберните компонент корневого приложения в новый компонент ErrorBoundary!
JavaScript ReactDOM.render( <ErrorBoundary> <App /> </ErrorBoundary>, document.getElementById(‘root’) )
123456 | ReactDOM.render( <ErrorBoundary> <App /> </ErrorBoundary>, document.getElementById(‘root’)) |
Обратите внимание, что вы можете использовать error boundaries так, чтобы они также отображали базовый макет (верхний колонтитул, нижний колонтитул и т. Д.).
Добавим функцию reset!
Иногда подобные ошибки случаются, когда пользовательский интерфейс переходит в нестабильное состояние. Всякий раз, когда возникает ошибка, все поддерево error boundary размонтируется, что, в свою очередь, сбрасывает любое внутреннее состояние.
Предоставление пользователю кнопки «Хочу повторить попытку», которая будет пытаться перемонтировать поддерево с новым состоянием, иногда может быть хорошей идеей! Давайте сделаем это.
JavaScript class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { if (this.state.hasError) { return ( <div> <h1>Oops, we done goofed up</h1> <button type=”button” onClick={() => this.setState({ hasError: false })}> Try again? </button> </div> ); } return this.props.children; } }
12345678910111213141516171819202122 | class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } render() { if (this.state.hasError) { return ( <div> <h1>Oops, we done goofed up</h1> <button type=”button” onClick={() => this.setState({ hasError: false })}> Try again? </button> </div> ); } return this.props.children; } } |
Конечно, это может быть не очень хорошей идеей для приложения. При реализации подобных функций учитывайте свои потребности и потребности пользователей.
Инструмент воспроизведения сеанса с открытым исходным кодом
Отладка веб-приложения в производственной среде может быть сложной задачей и потребовать много времени. OpenReplay — это альтернатива с открытым исходным кодом для FullStory, LogRocket и Hotjar. Он позволяет отслеживать и воспроизводить все, что делают ваши пользователи, и показывает, как ваше приложение ведет себя при каждой проблеме. Это похоже на то, как если бы инспектор вашего браузера был открыт, когда вы смотрите через плечо пользователя. OpenReplay — единственная доступная альтернатива с открытым исходным кодом.
Ограничения
Error boundaries отлично подходят для того, что они делают — вылавливают ошибки времени выполнения, которых вы не ожидали во время рендеринга. Однако есть несколько типов ошибок, которые не обнаруживаются, и с которыми нужно справляться другим способом. К ним относятся:
React JS. Основы
Изучите основы ReactJS на практическом примере по созданию учебного веб-приложения
Получить курс сейчас!
ошибки в обработчиках событий (например, при нажатии кнопки)
ошибки в асинхронных обратных вызовах (например, setTimeout)
ошибки, которые происходят в самом компоненте error boundary
ошибки, возникающие при рендеринге на стороне сервера
Эти ограничения могут показаться серьезными, но в большинстве случаев их можно обойти, используя try-catch и hasError.
JavaScript function SignUpButton(props) { const [hasError, setError] = React.useState(false); const handleClick = async () => { try { await api.signUp(); } catch(error) { errorService.log({ error }) setError(true); } } if (hasError) { return <p>Sign up failed!</p>; } return <button onClick={handleClick}>Sign up</button>; }
123456789101112131415 | function SignUpButton(props) { const [hasError, setError] = React.useState(false); const handleClick = async () => { try { await api.signUp(); } catch(error) { errorService.log({ error }) setError(true); } } if (hasError) { return <p>Sign up failed!</p>; } return <button onClick={handleClick}>Sign up</button>;} |
Это работает достаточно хорошо, даже если вам нужно продублировать несколько строк кода.
Создание лучшей Error boundary
Error boundaries хороши по умолчанию, но было бы неплохо повторно использовать их логику обработки ошибок в обработчиках событий и асинхронных местах. Это достаточно просто реализовать через контекстный API! Давайте реализуем функцию для запуска ошибок вручную.
JavaScript class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } triggerError = ({ error, errorInfo }) => { errorService.log({ error, errorInfo }); this.setState({ hasError: true }); } resetError = () => this.setState({ hasError: false }); render() { if (this.state.hasError) { return <h1>Oops, we done goofed up</h1>; } return this.props.children; } }
1234567891011121314151617181920 | class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } triggerError = ({ error, errorInfo }) => { errorService.log({ error, errorInfo }); this.setState({ hasError: true }); } resetError = () => this.setState({ hasError: false }); render() { if (this.state.hasError) { return <h1>Oops, we done goofed up</h1>; } return this.props.children; } } |
Затем давайте создадим контекст и передадим в него нашу новую функцию:
JavaScript const ErrorBoundaryContext = React.createContext(() => {});
1 | const ErrorBoundaryContext = React.createContext(() => {}); |
Затем мы можем создать пользовательский хук для извлечения функции запуска ошибки из любого дочернего компонента:
JavaScript const useErrorHandling = () => { return React.useContext(ErrorBoundaryContext) }
123 | const useErrorHandling = () => { return React.useContext(ErrorBoundaryContext)} |
Затем давайте обернем нашу error boundary в контексте:
JavaScript class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } triggerError = ({ error, errorInfo }) => { errorService.log({ error, errorInfo }); this.setState({ hasError: true }); } resetError = () => this.setState({ hasError: false }); render() { return ( <ErrorBoundaryContext.Provider value={this.triggerError}> {this.state.hasError ? <h1>Oops, we done goofed up</h1> : this.props.children } </ErrorBoundaryContext.Provider> ); } }
123456789101112131415161718192021222324 | class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } componentDidCatch(error, errorInfo) { errorService.log({ error, errorInfo }); } triggerError = ({ error, errorInfo }) => { errorService.log({ error, errorInfo }); this.setState({ hasError: true }); } resetError = () => this.setState({ hasError: false }); render() { return ( <ErrorBoundaryContext.Provider value={this.triggerError}> {this.state.hasError ? <h1>Oops, we done goofed up</h1> : this.props.children } </ErrorBoundaryContext.Provider> ); } } |
Теперь мы можем запускать ошибки и из наших обработчиков событий!
JavaScript function SignUpButton(props) { const { triggerError } = useErrorHandling(); const handleClick = async () => { try { await api.signUp(); } catch(error) { triggerError(error); } } return <button onClick={handleClick}>Sign up</button>; }
1234567891011 | function SignUpButton(props) { const { triggerError } = useErrorHandling(); const handleClick = async () => { try { await api.signUp(); } catch(error) { triggerError(error); } } return <button onClick={handleClick}>Sign up</button>;} |
Теперь нам не нужно думать об отчетах об ошибках или создании резервного пользовательского интерфейса для каждого реализованного нами обработчика кликов — все это находится в компоненте error boundary.
Использование react-error-boundary
Написание собственной логики error boundary, как мы делали выше, — это нормально, и вам подойдет большинство вариантов использования. Однако это решенная проблема. Член команды React Core Брайан Вон (а позже очень талантливый преподаватель React Кент С. Доддс) потратил немного времени на создание [react-error-boundary] (https://www.npmjs.com/package/react-error-boundary) пакета npm, который дает вам почти то же самое, что и выше.
API немного отличается, поэтому вы можете передавать пользовательские резервные компоненты и логику reset вместо написания своей собственной, но он используется очень похожим образом. Вот пример:
JavaScript ReactDOM.render( <ErrorBoundary FallbackComponent={MyFallbackComponent} onError={(error, errorInfo) => errorService.log({ error, errorInfo })} > <App /> </ErrorBoundary>, document.getElementById(‘root’) )
123456789 | ReactDOM.render( <ErrorBoundary FallbackComponent={MyFallbackComponent} onError={(error, errorInfo) => errorService.log({ error, errorInfo })} > <App /> </ErrorBoundary>, document.getElementById(‘root’)) |
Вы также можете посмотреть, как это реализовано — он использует другой подход к запуску ошибок из хуков, но в остальном все работает примерно так же.
Заключение
Обработка ошибок и неожиданных событий имеет решающее значение для любого качественного приложения. Чрезвычайно важно обеспечить удобство работы пользователей, даже если все идет не так, как планировалось.
Error boundaries — отличный способ заставить ваше приложение упасть изящно и даже содержать ошибки, которые привели к падению, в то время как остальная часть приложения продолжит работать! Напишите свой собственный или воспользуйтесь библиотекой react-error-boundary, которая сделает все за вас. Независимо от того, что вы выберете, пользователи будут вам благодарны!
Автор: Kristofer Selbekk
Источник: webformyself.com