Aprenda a usar ErrorBoundaries do React para lidar com erros de forma elegante, evitar que a aplicação quebre e proporcionar uma melhor experiência ao usuário com estratégias robustas de recuperação.
ErrorBoundary do React: Estratégias de Isolamento e Recuperação de Erros
No mundo dinâmico do desenvolvimento front-end, especialmente ao trabalhar com frameworks complexos baseados em componentes como o React, erros inesperados são inevitáveis. Esses erros, se não forem tratados corretamente, podem levar a quebras da aplicação e a uma experiência frustrante para o usuário. O componente ErrorBoundary do React oferece uma solução robusta para lidar com esses erros de forma elegante, isolando-os e fornecendo estratégias de recuperação. Este guia completo explora o poder do ErrorBoundary, demonstrando como implementá-lo efetivamente para construir aplicações React mais resilientes e amigáveis para um público global.
Entendendo a Necessidade de Error Boundaries
Antes de mergulhar na implementação, vamos entender por que os error boundaries são essenciais. No React, erros que ocorrem durante a renderização, em métodos de ciclo de vida ou em construtores de componentes filhos podem potencialmente quebrar toda a aplicação. Isso ocorre porque erros não capturados se propagam pela árvore de componentes, muitas vezes resultando em uma tela em branco ou uma mensagem de erro inútil. Imagine um usuário no Japão tentando concluir uma transação financeira importante, apenas para se deparar com uma tela em branco devido a um erro menor em um componente aparentemente não relacionado. Isso ilustra a necessidade crítica de um gerenciamento proativo de erros.
Error boundaries fornecem uma maneira de capturar erros de JavaScript em qualquer lugar na sua árvore de componentes filhos, registrar esses erros e exibir uma UI de fallback em vez de quebrar a árvore de componentes. Eles permitem que você isole componentes defeituosos e evite que erros em uma parte da sua aplicação afetem outras, garantindo uma experiência de usuário mais estável e confiável globalmente.
O que é um ErrorBoundary do React?
Um ErrorBoundary é um componente React que captura erros de JavaScript em qualquer lugar na sua árvore de componentes filhos, registra esses erros e exibe uma UI de fallback. É um componente de classe que implementa um ou ambos os seguintes métodos de ciclo de vida:
static getDerivedStateFromError(error): Este método de ciclo de vida é invocado após um erro ser lançado por um componente descendente. Ele recebe o erro que foi lançado como argumento e deve retornar um valor para atualizar o estado do componente.componentDidCatch(error, info): Este método de ciclo de vida é invocado após um erro ser lançado por um componente descendente. Ele recebe dois argumentos: o erro que foi lançado e um objeto de informação contendo detalhes sobre qual componente lançou o erro. Você pode usar este método para registrar informações de erro ou realizar outros efeitos colaterais.
Criando um Componente ErrorBoundary Básico
Vamos criar um componente ErrorBoundary básico para ilustrar os princípios fundamentais.
Exemplo de Código
Aqui está o código para um componente ErrorBoundary simples:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error) {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// Exemplo de "componentStack":
// in ComponentThatThrows (created by App)
// in App
console.error("Caught an error:", error);
console.error("Error info:", info.componentStack);
this.setState({ error: error, errorInfo: info });
// Você também pode registrar o erro em um serviço de relatórios de erros
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return (
Algo deu errado.
Erro: {this.state.error && this.state.error.toString()}
{this.state.errorInfo && this.state.errorInfo.componentStack}
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Explicação
- Construtor: O construtor inicializa o estado do componente com
hasErrordefinido comofalse. Também armazenamos o erro e as informações do erro para fins de depuração. getDerivedStateFromError(error): Este método estático é invocado quando um erro é lançado por um componente filho. Ele atualiza o estado para indicar que um erro ocorreu.componentDidCatch(error, info): Este método é invocado após um erro ser lançado. Ele recebe o erro e um objetoinfocontendo informações sobre a pilha de componentes. Aqui, registramos o erro no console (substitua pelo seu mecanismo de log preferido, como Sentry, Bugsnag ou uma solução interna personalizada). Também definimos o erro e as informações do erro no estado.render(): O método render verifica o estadohasError. Se fortrue, ele renderiza uma UI de fallback; caso contrário, ele renderiza os filhos do componente. A UI de fallback deve ser informativa e amigável. Incluir os detalhes do erro e a pilha de componentes, embora útil para desenvolvedores, deve ser renderizado condicionalmente ou removido em ambientes de produção por razões de segurança.
Usando o Componente ErrorBoundary
Para usar o componente ErrorBoundary, basta envolver qualquer componente que possa lançar um erro dentro dele.
Exemplo de Código
import ErrorBoundary from './ErrorBoundary';
function MyComponent() {
return (
{/* Componentes que podem lançar um erro */}
);
}
function App() {
return (
);
}
export default App;
Explicação
Neste exemplo, MyComponent é envolvido pelo ErrorBoundary. Se qualquer erro ocorrer dentro de MyComponent ou de seus filhos, o ErrorBoundary o capturará e renderizará a UI de fallback.
Estratégias Avançadas de ErrorBoundary
Embora o ErrorBoundary básico forneça um nível fundamental de tratamento de erros, existem várias estratégias avançadas que você pode implementar para aprimorar seu gerenciamento de erros.
1. Error Boundaries Granulares
Em vez de envolver toda a aplicação com um único ErrorBoundary, considere usar error boundaries granulares. Isso envolve colocar componentes ErrorBoundary em torno de partes específicas da sua aplicação que são mais propensas a erros ou onde a falha teria um impacto limitado. Por exemplo, você pode envolver widgets individuais ou componentes que dependem de fontes de dados externas.
Exemplo
function ProductList() {
return (
{/* Lista de produtos */}
);
}
function RecommendationWidget() {
return (
{/* Mecanismo de recomendação */}
);
}
function App() {
return (
);
}
Neste exemplo, o RecommendationWidget tem seu próprio ErrorBoundary. Se o mecanismo de recomendação falhar, ele não afetará a ProductList, e o usuário ainda poderá navegar pelos produtos. Essa abordagem granular melhora a experiência geral do usuário, isolando erros e evitando que eles se propaguem pela aplicação.
2. Registro e Relatório de Erros
Registrar erros é crucial para depurar e identificar problemas recorrentes. O método de ciclo de vida componentDidCatch é o local ideal para integrar com serviços de registro de erros como Sentry, Bugsnag ou Rollbar. Esses serviços fornecem relatórios de erro detalhados, incluindo rastreamentos de pilha, contexto do usuário e informações do ambiente, permitindo que você diagnostique e resolva problemas rapidamente. Considere anonimizar ou redigir dados sensíveis do usuário antes de enviar logs de erro para garantir a conformidade com regulamentos de privacidade como o GDPR.
Exemplo
import * as Sentry from "@sentry/react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
};
}
static getDerivedStateFromError(error) {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return {
hasError: true,
};
}
componentDidCatch(error, info) {
// Registra o erro no Sentry
Sentry.captureException(error, { extra: info });
// Você também pode registrar o erro em um serviço de relatórios de erros
console.error("Caught an error:", error);
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return (
Algo deu errado.
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Neste exemplo, o método componentDidCatch usa Sentry.captureException para relatar o erro ao Sentry. Você pode configurar o Sentry para enviar notificações para sua equipe, permitindo que você responda rapidamente a erros críticos.
3. UI de Fallback Personalizada
A UI de fallback exibida pelo ErrorBoundary é uma oportunidade para fornecer uma experiência amigável mesmo quando ocorrem erros. Em vez de mostrar uma mensagem de erro genérica, considere exibir uma mensagem mais informativa que guie o usuário em direção a uma solução. Isso pode incluir instruções sobre como atualizar a página, contatar o suporte ou tentar novamente mais tarde. Você também pode personalizar a UI de fallback com base no tipo de erro que ocorreu.
Exemplo
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
};
}
static getDerivedStateFromError(error) {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback.
return {
hasError: true,
error: error,
};
}
componentDidCatch(error, info) {
console.error("Caught an error:", error);
// Você também pode registrar o erro em um serviço de relatórios de erros
// logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
if (this.state.error instanceof NetworkError) {
return (
Erro de Rede
Por favor, verifique sua conexão com a internet e tente novamente.
);
} else {
return (
Algo deu errado.
Por favor, tente atualizar a página ou contate o suporte.
);
}
}
return this.props.children;
}
}
export default ErrorBoundary;
Neste exemplo, a UI de fallback verifica se o erro é um NetworkError. Se for, exibe uma mensagem específica instruindo o usuário a verificar sua conexão com a internet. Caso contrário, exibe uma mensagem de erro genérica. Fornecer orientação específica e acionável pode melhorar muito a experiência do usuário.
4. Mecanismos de Tentativa (Retry)
Em alguns casos, os erros são transitórios e podem ser resolvidos tentando novamente a operação. Você pode implementar um mecanismo de tentativa dentro do ErrorBoundary para tentar novamente a operação com falha automaticamente após um certo atraso. Isso pode ser particularmente útil para lidar com erros de rede ou interrupções temporárias do servidor. Tenha cuidado ao implementar mecanismos de tentativa para operações que possam ter efeitos colaterais, pois tentar novamente pode levar a consequências não intencionais.
Exemplo
import React, { useState, useEffect } from 'react';
function DataFetchingComponent() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
try {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
setError(null);
} catch (e) {
setError(e);
setRetryCount(prevCount => prevCount + 1);
} finally {
setIsLoading(false);
}
};
if (error && retryCount < 3) {
const retryDelay = Math.pow(2, retryCount) * 1000; // Backoff exponencial
console.log(`Tentando novamente em ${retryDelay / 1000} segundos...`);
const timer = setTimeout(fetchData, retryDelay);
return () => clearTimeout(timer); // Limpa o timer na desmontagem ou re-renderização
}
if (!data) {
fetchData();
}
}, [error, retryCount, data]);
if (isLoading) {
return Carregando dados...
;
}
if (error) {
return Erro: {error.message} - Tentativas: {retryCount}.
;
}
return Dados: {JSON.stringify(data)}
;
}
function App() {
return (
);
}
export default App;
Neste exemplo, o DataFetchingComponent tenta buscar dados de uma API. Se ocorrer um erro, ele incrementa o retryCount e tenta novamente a operação após um atraso que aumenta exponencialmente. O ErrorBoundary captura quaisquer exceções não tratadas e exibe uma mensagem de erro, incluindo o número de tentativas.
5. Error Boundaries e Renderização no Lado do Servidor (SSR)
Ao usar a Renderização no Lado do Servidor (SSR), o tratamento de erros se torna ainda mais crítico. Erros que ocorrem durante o processo de renderização no lado do servidor podem quebrar todo o servidor, levando a tempo de inatividade e uma má experiência do usuário. Você precisa garantir que seus error boundaries estejam configurados corretamente para capturar erros tanto no servidor quanto no cliente. Frequentemente, frameworks de SSR como Next.js e Remix têm seus próprios mecanismos de tratamento de erros integrados que complementam os Error Boundaries do React.
6. Testando Error Boundaries
Testar error boundaries é essencial para garantir que eles funcionem corretamente e forneçam a UI de fallback esperada. Use bibliotecas de teste como Jest e React Testing Library para simular condições de erro e verificar se seus error boundaries capturam os erros e renderizam a UI de fallback apropriada. Considere testar diferentes tipos de erros e casos extremos para garantir que seus error boundaries sejam robustos e lidem com uma ampla gama de cenários.
Exemplo
import { render, screen } from '@testing-library/react';
import ErrorBoundary from './ErrorBoundary';
function ComponentThatThrows() {
throw new Error('Este componente lança um erro');
return Isso não deve ser renderizado
;
}
test('renderiza a UI de fallback quando um erro é lançado', () => {
render(
);
const errorMessage = screen.getByText(/Algo deu errado/i);
expect(errorMessage).toBeInTheDocument();
});
Este teste renderiza um componente que lança um erro dentro de um ErrorBoundary. Em seguida, ele verifica se a UI de fallback é renderizada corretamente, verificando se a mensagem de erro está presente no documento.
7. Degradação Graciosa
Error boundaries são um componente chave na implementação da degradação graciosa em suas aplicações React. Degradação graciosa é a prática de projetar sua aplicação para continuar funcionando, embora com funcionalidade reduzida, mesmo quando partes dela falham. Error boundaries permitem que você isole componentes com falha e evite que eles afetem o resto da aplicação. Ao fornecer uma UI de fallback e funcionalidade alternativa, você pode garantir que os usuários ainda possam acessar recursos essenciais mesmo quando ocorrem erros.
Armadilhas Comuns a Evitar
Embora o ErrorBoundary seja uma ferramenta poderosa, existem algumas armadilhas comuns a serem evitadas:
- Não envolver código assíncrono:
ErrorBoundarysó captura erros durante a renderização, em métodos de ciclo de vida e em construtores. Erros em código assíncrono (por exemplo,setTimeout,Promises) precisam ser capturados usando blocostry...catche tratados apropriadamente dentro da função assíncrona. - Uso excessivo de Error Boundaries: Evite envolver grandes partes de sua aplicação em um único
ErrorBoundary. Isso pode dificultar o isolamento da origem dos erros e pode levar a uma UI de fallback genérica sendo exibida com muita frequência. Use error boundaries granulares para isolar componentes ou recursos específicos. - Ignorar informações de erro: Não apenas capture erros e exiba uma UI de fallback. Certifique-se de registrar as informações do erro (incluindo a pilha de componentes) em um serviço de relatórios de erros ou no seu console. Isso ajudará você a diagnosticar e corrigir os problemas subjacentes.
- Exibir informações sensíveis em produção: Evite exibir informações detalhadas de erro (por exemplo, rastreamentos de pilha) em ambientes de produção. Isso pode expor informações sensíveis aos usuários e pode ser um risco de segurança. Em vez disso, exiba uma mensagem de erro amigável e registre as informações detalhadas em um serviço de relatórios de erros.
Error Boundaries com Componentes Funcionais e Hooks
Embora os Error Boundaries sejam implementados como componentes de classe, você ainda pode usá-los efetivamente para tratar erros em componentes funcionais que usam hooks. A abordagem típica envolve envolver o componente funcional dentro de um componente ErrorBoundary, como demonstrado anteriormente. A lógica de tratamento de erros reside dentro do ErrorBoundary, isolando efetivamente os erros que podem ocorrer durante a renderização do componente funcional ou a execução de hooks.
Especificamente, quaisquer erros lançados durante a renderização do componente funcional ou dentro do corpo de um hook useEffect serão capturados pelo ErrorBoundary. No entanto, é importante notar que os ErrorBoundaries não capturam erros que ocorrem dentro de manipuladores de eventos (por exemplo, onClick, onChange) anexados a elementos DOM dentro do componente funcional. Para manipuladores de eventos, você deve continuar a usar blocos try...catch tradicionais para o tratamento de erros.
Internacionalização e Localização de Mensagens de Erro
Ao desenvolver aplicações para um público global, é crucial internacionalizar e localizar suas mensagens de erro. As mensagens de erro exibidas na UI de fallback do ErrorBoundary devem ser traduzidas para o idioma preferido do usuário para fornecer uma melhor experiência. Você pode usar bibliotecas como i18next ou React Intl para gerenciar suas traduções e exibir dinamicamente a mensagem de erro apropriada com base na localidade do usuário.
Exemplo usando i18next
import i18next from 'i18next';
import { useTranslation } from 'react-i18next';
i18next.init({
resources: {
en: {
translation: {
'error.generic': 'Algo deu errado. Por favor, tente novamente mais tarde.',
'error.network': 'Erro de rede. Por favor, verifique sua conexão com a internet.',
},
},
fr: {
translation: {
'error.generic': 'Une erreur est survenue. Veuillez réessayer plus tard.',
'error.network': 'Erreur réseau. Veuillez vérifier votre connexion Internet.',
},
},
},
lng: 'en',
fallbackLng: 'en',
interpolation: {
escapeValue: false, // não é necessário para o React, pois ele já faz o escape por padrão
},
});
function ErrorFallback({ error }) {
const { t } = useTranslation();
let errorMessageKey = 'error.generic';
if (error instanceof NetworkError) {
errorMessageKey = 'error.network';
}
return (
{t('error.generic')}
{t(errorMessageKey)}
);
}
function ErrorBoundary({ children }) {
const [hasError, setHasError] = useState(false);
const [error, setError] = useState(null);
static getDerivedStateFromError = (error) => {
// Atualiza o estado para que a próxima renderização mostre a UI de fallback
// return { hasError: true }; // isso não funciona com hooks como está
setHasError(true);
setError(error);
}
if (hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return ;
}
return children;
}
export default ErrorBoundary;
Neste exemplo, usamos i18next para gerenciar traduções para inglês e francês. O componente ErrorFallback usa o hook useTranslation para recuperar a mensagem de erro apropriada com base no idioma atual. Isso garante que os usuários vejam mensagens de erro em seu idioma preferido, aprimorando a experiência geral do usuário.
Conclusão
Os componentes ErrorBoundary do React são uma ferramenta crucial para construir aplicações React robustas e amigáveis. Ao implementar error boundaries, você pode lidar com erros de forma elegante, evitar que a aplicação quebre e proporcionar uma melhor experiência para usuários em todo o mundo. Ao entender os princípios dos error boundaries, implementar estratégias avançadas como error boundaries granulares, registro de erros e UIs de fallback personalizadas, e evitar armadilhas comuns, você pode construir aplicações React mais resilientes e confiáveis que atendam às necessidades de um público global. Lembre-se de considerar a internacionalização и a localização ao exibir mensagens de erro para fornecer uma experiência de usuário verdadeiramente inclusiva. À medida que a complexidade das aplicações web continua a crescer, dominar as técnicas de tratamento de erros se tornará cada vez mais importante para desenvolvedores que constroem software de alta qualidade.