Domine o React Suspense com padrões práticos para busca de dados eficiente, estados de carregamento e tratamento robusto de erros. Crie experiências de usuário mais fluidas e resilientes.
Padrões de React Suspense: Busca de Dados e Error Boundaries
O React Suspense é um recurso poderoso que permite "suspender" a renderização de componentes enquanto se aguarda a conclusão de operações assíncronas, como a busca de dados. Em conjunto com os Error Boundaries, ele fornece um mecanismo robusto para lidar com estados de carregamento e erros, resultando em uma experiência de usuário mais fluida e resiliente. Este artigo explora vários padrões para aproveitar o Suspense e os Error Boundaries de forma eficaz em suas aplicações React.
Entendendo o React Suspense
Em sua essência, o Suspense é um mecanismo que permite ao React esperar por algo antes de renderizar um componente. Esse "algo" é tipicamente uma operação assíncrona, como buscar dados de uma API. Em vez de exibir uma tela em branco ou um estado intermediário potencialmente enganoso, você pode exibir uma UI de fallback (por exemplo, um spinner de carregamento) enquanto os dados estão sendo carregados.
O principal benefício é a melhoria na performance percebida e uma experiência de usuário mais agradável. Os usuários recebem imediatamente um feedback visual indicando que algo está acontecendo, em vez de se perguntarem se a aplicação travou.
Conceitos Chave
- Componente Suspense: O componente
<Suspense>envolve componentes que podem suspender. Ele aceita uma propfallback, que especifica a UI a ser renderizada enquanto os componentes envolvidos estão suspensos. - UI de Fallback: Esta é a UI exibida enquanto a operação assíncrona está em andamento. Pode ser qualquer coisa, desde um simples spinner de carregamento até uma animação mais elaborada.
- Integração com Promises: O Suspense funciona com Promises. Quando um componente tenta ler um valor de uma Promise que ainda não foi resolvida, o React suspende o componente e exibe a UI de fallback.
- Fontes de Dados: O Suspense depende de fontes de dados que sejam compatíveis com ele. Essas fontes expõem uma API que permite ao React detectar quando os dados estão sendo buscados.
Buscando Dados com Suspense
Para usar o Suspense para busca de dados, você precisará de uma biblioteca de busca de dados compatível com o Suspense. Aqui está uma abordagem comum usando uma função fetchData personalizada:
Exemplo: Busca Simples de Dados
Primeiro, crie uma função utilitária para buscar dados. Esta função deve lidar com o aspecto de 'suspensão'. Vamos envolver nossas chamadas de fetch em um recurso personalizado para lidar corretamente com o estado da promise.
// utils/api.js
const wrapPromise = (promise) => {
let status = 'pending';
let result;
let suspender = promise.then(
(r) => {
status = 'success';
result = r;
},
(e) => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
}
return result;
},
};
};
const fetchData = (url) => {
const promise = fetch(url)
.then((res) => res.json())
.then((data) => data);
return wrapPromise(promise);
};
export default fetchData;
Agora, vamos criar um componente que usa o Suspense para exibir dados do usuário:
// components/UserProfile.js
import React from 'react';
import fetchData from '../utils/api';
const resource = fetchData('https://jsonplaceholder.typicode.com/users/1');
function UserProfile() {
const user = resource.read();
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
</div>
);
}
export default UserProfile;
Finalmente, envolva o componente UserProfile com <Suspense>:
// App.js
import React, { Suspense } from 'react';
import UserProfile from './components/UserProfile';
function App() {
return (
<Suspense fallback={<p>Carregando dados do usuário...</p>}>
<UserProfile />
</Suspense>
);
}
export default App;
Neste exemplo, o componente UserProfile tenta ler os dados do user do resource. Se os dados ainda não estiverem disponíveis (a Promise ainda está pendente), o componente suspende, e a UI de fallback ("Carregando dados do usuário...") é exibida. Assim que os dados são buscados, o componente é renderizado novamente com as informações reais do usuário.
Benefícios desta abordagem
- Busca de dados declarativa: O componente expressa *o que* ele precisa de dados, não *como* buscá-los.
- Gerenciamento centralizado do estado de carregamento: O componente Suspense lida com o estado de carregamento, simplificando a lógica do componente.
Error Boundaries para Resiliência
Embora o Suspense lide com estados de carregamento de forma elegante, ele não lida nativamente com erros que podem ocorrer durante a busca de dados ou a renderização de componentes. É aí que entram os Error Boundaries.
Error Boundaries são componentes React que capturam erros de JavaScript em qualquer lugar de sua árvore de componentes filhos, registram esses erros e exibem uma UI de fallback em vez de quebrar toda a aplicação. Eles são cruciais para construir UIs resilientes que podem lidar com erros inesperados de forma elegante.
Criando um Error Boundary
Para criar um Error Boundary, você precisa definir um componente de classe que implemente os métodos de ciclo de vida static getDerivedStateFromError() e componentDidCatch().
// components/ErrorBoundary.js
import React from 'react';
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,
error: error
};
}
componentDidCatch(error, errorInfo) {
// Você também pode registrar o erro em um serviço de relatórios de erros
console.error("Caught error: ", error, errorInfo);
this.setState({errorInfo: errorInfo});
}
render() {
if (this.state.hasError) {
// Você pode renderizar qualquer UI de fallback personalizada
return (
<div>
<h2>Algo deu errado.</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}<br />
{this.state.errorInfo.componentStack}
</details>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
O método getDerivedStateFromError é chamado quando um erro é lançado em um componente descendente. Ele atualiza o estado para indicar que um erro ocorreu.
O método componentDidCatch é chamado após um erro ter sido lançado. Ele recebe o erro e informações sobre o erro, que você pode usar para registrar o erro em um serviço de relatórios de erros ou exibir uma mensagem de erro mais informativa.
Usando Error Boundaries com Suspense
Para combinar Error Boundaries com Suspense, simplesmente envolva o componente <Suspense> com um componente <ErrorBoundary>:
// App.js
import React, { Suspense } from 'react';
import UserProfile from './components/UserProfile';
import ErrorBoundary from './components/ErrorBoundary';
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<p>Carregando dados do usuário...</p>}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}
export default App;
Agora, se ocorrer um erro durante a busca de dados ou a renderização do componente UserProfile, o Error Boundary capturará o erro e exibirá a UI de fallback, evitando que toda a aplicação quebre.
Padrões Avançados de Suspense
Além da busca de dados básica e do tratamento de erros, o Suspense oferece vários padrões avançados para construir UIs mais sofisticadas.
Code Splitting com Suspense
Code splitting é o processo de dividir sua aplicação em pedaços menores (chunks) que podem ser carregados sob demanda. Isso pode melhorar significativamente o tempo de carregamento inicial da sua aplicação.
O React.lazy e o Suspense tornam o code splitting incrivelmente fácil. Você pode usar o React.lazy para importar componentes dinamicamente e, em seguida, envolvê-los com <Suspense> para exibir uma UI de fallback enquanto os componentes estão sendo carregados.
// components/MyComponent.js
import React from 'react';
const MyComponent = React.lazy(() => import('./AnotherComponent'));
function App() {
return (
<Suspense fallback={<p>Carregando componente...</p>}>
<MyComponent />
</Suspense>
);
}
export default App;
Neste exemplo, o MyComponent é carregado sob demanda. Enquanto ele está sendo carregado, a UI de fallback ("Carregando componente...") é exibida. Assim que o componente é carregado, ele é renderizado normalmente.
Busca de Dados em Paralelo
O Suspense permite que você busque múltiplas fontes de dados em paralelo e exiba uma única UI de fallback enquanto todos os dados estão sendo carregados. Isso pode ser útil quando você precisa buscar dados de várias APIs para renderizar um único componente.
import React, { Suspense } from 'react';
import fetchData from './api';
const userResource = fetchData('https://jsonplaceholder.typicode.com/users/1');
const postsResource = fetchData('https://jsonplaceholder.typicode.com/posts?userId=1');
function UserProfile() {
const user = userResource.read();
const posts = postsResource.read();
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<h3>Posts:</h3>
<ul>
{posts.map(post => (<li key={post.id}>{post.title}</li>))}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback={<p>Carregando dados do usuário e posts...</p>}>
<UserProfile />
</Suspense>
);
}
export default App;
Neste exemplo, o componente UserProfile busca tanto os dados do usuário quanto os dados das postagens em paralelo. O componente <Suspense> exibe uma única UI de fallback enquanto ambas as fontes de dados estão sendo carregadas.
API de Transição com useTransition
O React 18 introduziu o hook useTransition, que aprimora o Suspense ao fornecer uma maneira de gerenciar atualizações de UI como transições. Isso significa que você pode marcar certas atualizações de estado como menos urgentes e evitar que elas bloqueiem a UI. Isso é especialmente útil ao lidar com buscas de dados mais lentas ou operações de renderização complexas, melhorando a performance percebida.
Veja como você pode usar o useTransition:
import React, { useState, Suspense, useTransition } from 'react';
import fetchData from './api';
const resource = fetchData('https://jsonplaceholder.typicode.com/users/1');
function UserProfile() {
const user = resource.read();
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Phone: {user.phone}</p>
</div>
);
}
function App() {
const [isPending, startTransition] = useTransition();
const [showProfile, setShowProfile] = useState(false);
const handleClick = () => {
startTransition(() => {
setShowProfile(true);
});
};
return (
<div>
<button onClick={handleClick} disabled={isPending}>
Mostrar Perfil do Usuário
</button>
{isPending && <p>Carregando...</p>}
<Suspense fallback={<p>Carregando dados do usuário...</p>}>
{showProfile && <UserProfile />}
</Suspense>
</div>
);
}
export default App;
Neste exemplo, clicar no botão “Mostrar Perfil do Usuário” inicia uma transição. startTransition marca a atualização setShowProfile como uma transição, permitindo que o React priorize outras atualizações da UI. O valor isPending de useTransition indica se uma transição está em andamento, permitindo que você forneça feedback visual (por exemplo, desabilitando o botão e mostrando uma mensagem de carregamento).
Melhores Práticas para Usar Suspense e Error Boundaries
- Envolva o Suspense na menor área possível: Evite envolver grandes partes da sua aplicação com
<Suspense>. Em vez disso, envolva apenas os componentes que realmente precisam suspender. Isso minimizará o impacto no restante da UI. - Use UIs de fallback significativas: A UI de fallback deve fornecer aos usuários um feedback claro e informativo sobre o que está acontecendo. Evite spinners de carregamento genéricos; em vez disso, tente fornecer mais contexto (por exemplo, "Carregando dados do usuário...").
- Posicione os Error Boundaries estrategicamente: Pense cuidadosamente sobre onde posicionar os Error Boundaries. Coloque-os em um nível alto o suficiente na árvore de componentes para capturar erros que possam afetar múltiplos componentes, mas baixo o suficiente para evitar capturar erros específicos de um único componente.
- Registre os erros: Use o método
componentDidCatchpara registrar erros em um serviço de relatórios de erros. Isso ajudará você a identificar e corrigir erros em sua aplicação. - Forneça mensagens de erro amigáveis: A UI de fallback exibida pelos Error Boundaries deve fornecer aos usuários informações úteis sobre o erro e o que eles podem fazer a respeito. Evite jargões técnicos; em vez disso, use uma linguagem clara e concisa.
- Teste seus Error Boundaries: Certifique-se de que seus Error Boundaries estão funcionando corretamente, lançando erros deliberadamente em sua aplicação.
Considerações Internacionais
Ao usar Suspense e Error Boundaries em aplicações internacionais, considere o seguinte:
- Localização: Garanta que as UIs de fallback e as mensagens de erro sejam devidamente localizadas para cada idioma suportado pela sua aplicação. Use bibliotecas de internacionalização (i18n) como
react-intloui18nextpara gerenciar as traduções. - Layouts da direita para a esquerda (RTL): Se sua aplicação suporta idiomas RTL (por exemplo, árabe, hebraico), garanta que as UIs de fallback e as mensagens de erro sejam exibidas corretamente em layouts RTL. Use propriedades lógicas de CSS (por exemplo,
margin-inline-startem vez demargin-left) para suportar tanto layouts LTR quanto RTL. - Acessibilidade: Garanta que as UIs de fallback e as mensagens de erro sejam acessíveis a usuários com deficiência. Use atributos ARIA para fornecer informações semânticas sobre o estado de carregamento e as mensagens de erro.
- Sensibilidade cultural: Esteja ciente das diferenças culturais ao projetar UIs de fallback e mensagens de erro. Evite usar imagens ou linguagem que possam ser ofensivas ou inadequadas em certas culturas. Por exemplo, um spinner de carregamento comum pode ser percebido negativamente em algumas culturas.
Exemplo: Mensagem de Erro Localizada
Usando react-intl, você pode criar mensagens de erro localizadas:
// components/ErrorBoundary.js
import React from 'react';
import { FormattedMessage } from 'react-intl';
class ErrorBoundary extends React.Component {
// ... (o mesmo de antes)
render() {
if (this.state.hasError) {
return (
<div>
<h2><FormattedMessage id="error.title" defaultMessage="Algo deu errado." /></h2>
<p><FormattedMessage id="error.message" defaultMessage="Por favor, tente novamente mais tarde." /></p>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Então, defina as traduções em seus arquivos de localidade:
// locales/en.json
{
"error.title": "Something went wrong.",
"error.message": "Please try again later."
}
// locales/pt-BR.json
{
"error.title": "Algo deu errado.",
"error.message": "Por favor, tente novamente mais tarde."
}
Conclusão
O React Suspense e os Error Boundaries são ferramentas essenciais para construir UIs modernas, resilientes e amigáveis ao usuário. Ao entender e aplicar os padrões descritos neste artigo, você pode melhorar significativamente a performance percebida e a qualidade geral de suas aplicações React. Lembre-se de considerar a internacionalização e a acessibilidade para garantir que suas aplicações sejam utilizáveis por um público global.
A busca de dados assíncrona e o tratamento adequado de erros são aspectos críticos de qualquer aplicação web. O Suspense, combinado com Error Boundaries, oferece uma maneira declarativa e eficiente de gerenciar essas complexidades no React, resultando em uma experiência de usuário mais fluida e confiável para usuários em todo o mundo.