Juntao Qiu React Anti Patterns Build Efficient and Maintainable React Applications With Test Driven 30 46 PT
Juntao Qiu React Anti Patterns Build Efficient and Maintainable React Applications With Test Driven 30 46 PT
Juntao Qiu React Anti Patterns Build Efficient and Maintainable React Applications With Test Driven 30 46 PT
Ao longo do livro, examinaremos exemplos de código que podem não incorporar as práticas
recomendadas; alguns podem ser difíceis de decifrar e outros, difíceis de modificar ou estender.
Embora certos trechos de código possam ser suficientes para tarefas menores, eles vacilam quando são
ampliados. Além disso, nos aventuraremos em padrões e princípios testados pelo tempo do mundo do
software expansivo, incorporando-os perfeitamente ao nosso discurso de front-end.
Requisitos técnicos
Um repositório do GitHub foi criado para hospedar todo o código que discutimos no livro. Para este
capítulo, você pode encontrar o código em https://github.com/PacktPublishing/React-Anti-
Patterns/tree/main/code/src/ch1.
No entanto, hoje em dia, a maioria dos aplicativos é mais complicada e contém mais elementos do
que os originalmente previstos para essa linguagem.
Conforme mostrado na Figura 1.2, uma página da Web pode ser muito complicada e não se parecer
em nada com um documento na superfície, embora os blocos de construção da página ainda sejam
HTML puro:
Esta captura de tela mostra a visualização de problemas do Jira, uma popular ferramenta de
gerenciamento de projetos baseada na Web usada para rastrear, priorizar e coordenar tarefas e
projetos. Uma visualização de problema contém muitos detalhes, como o título do problema, a
descrição, os anexos, os comentários e os problemas vinculados. Ela também contém muitos
elementos com os quais o usuário pode interagir, como o botão Atribuir a mim, a capacidade de
alterar a prioridade do problema, adicionar um comentário e assim por diante.
Para uma interface de usuário desse tipo, é de se esperar que haja um componente de navegação, uma
lista suspensa, um acordeão e assim por diante. E, aparentemente, eles estão lá, conforme indicado na
Figura 1.2. Mas eles não são componentes de fato. Em vez disso, os desenvolvedores se esforçaram
muito para simulá-los com HTML, CSS e JavaScript.
Agora que já examinamos o problema da incompatibilidade de linguagens no desenvolvimento da
interface do usuário da Web, talvez seja útil nos aprofundarmos no que está sob a superfície: os
diferentes estados que precisamos gerenciar nos aplicativos front-end. Isso nos dará uma ideia dos
desafios que temos pela frente e esclarecerá por que a introdução de padrões é uma etapa
fundamental para enfrentá-los.
Há muitos lados obscuros dos estados remotos, o que dificulta o desenvolvimento de front-end se
você não prestar muita atenção a eles. Aqui, listarei apenas algumas considerações óbvias:
Natureza assíncrona: A obtenção de dados de uma fonte remota geralmente é uma operação assíncrona. Isso aumenta a
complexidade em termos de tempo, especialmente quando você precisa sincronizar várias partes de dados remotos.
Tratamento de erros: As conexões com fontes remotas podem falhar ou o servidor pode retornar erros. Gerenciar
adequadamente esses cenários para proporcionar uma experiência de usuário tranquila pode ser um desafio.
Estados de carregamento: Enquanto aguarda a chegada de dados de uma fonte remota, o aplicativo precisa lidar com os estados
de "carregamento" de forma eficaz. Em geral, isso envolve a exibição de indicadores de carregamento ou IUs de fallback
(quando o componente solicitante não está disponível, usamos um componente padrão temporariamente).
Consistência: Manter o estado do front-end em sincronia com o back-end pode ser difícil, especialmente em aplicativos em tempo
real ou naqueles que envolvem vários usuários alterando a mesma parte dos dados.
Armazenamento em cache: armazenar algum estado remoto localmente pode melhorar o desempenho, mas traz seus próprios
desafios, como invalidação e obsoletismo. Em outras palavras, se os dados remotos forem alterados por outras pessoas,
precisaremos de um mecanismo para receber atualizações ou executar uma nova busca para atualizar nosso estado local, o que
introduz muita complexidade.
Atualizações e UI otimista: Quando um usuário faz uma alteração, você pode atualizar a interface do usuário de forma
otimista, presumindo que a chamada do servidor será bem-sucedida. Mas, se não for bem-sucedida, você precisará de uma
maneira de reverter essas alterações no estado do frontend.
Quando os dados são armazenados e acessíveis imediatamente no frontend, você basicamente pensa
de forma linear. Isso significa que você acessa e manipula os dados em uma sequência direta, uma
operação seguindo a outra, levando a um fluxo claro e direto da lógica. Essa forma de pensar se
alinha bem com a natureza síncrona do código, tornando o processo de desenvolvimento intuitivo e
mais fácil de acompanhar.
Vamos comparar quanto código a mais precisaremos para renderizar dados estáticos com dados
remotos. Pense em um aplicativo de citações famosas que exibe uma lista de citações na página.
Para renderizar a lista de cotações passada, você pode mapear os dados em elementos JSX, da seguinte
forma:
function Quotes(quotes: string[]) {
return (
<ul>
{quotes.map((quote, index) => <li key={index}>{quote}</li>)}
</ul>
);
}
OBSERVAÇÃO
Estamos usando index como chave aqui, o que é bom para citações estáticas. No entanto, geralmente é melhor evitar
essa prática. O uso de índices pode levar a problemas de renderização em listas dinâmicas em cenários reais.
Nesse componente React, usamos useState para criar uma variável de estado de cotações,
inicialmente definida como uma matriz vazia. O Hook useEffect obtém as cotações de um servidor
remoto quando o componente é montado. Em seguida, ele atualiza o estado das cotações com os
dados obtidos. Por fim, o componente renderiza uma lista de cotações, percorrendo a matriz de
cotações.
Não se preocupe, não há necessidade de se preocupar com os detalhes por enquanto; vamos nos
aprofundar neles no próximo capítulo sobre os fundamentos do React.
O exemplo de código anterior mostra o cenário ideal, mas, na realidade, as chamadas assíncronas têm
seus próprios desafios. Temos que pensar sobre o que exibir enquanto os dados estão sendo obtidos e
como lidar com vários cenários de erro, como problemas de rede ou indisponibilidade de recursos.
Essas complexidades adicionais podem tornar o código mais longo e mais difícil de entender.
Por exemplo, ao buscar dados, fazemos a transição temporária para um estado de carregamento e, se
algo der errado, passamos para um estado de erro:
function Quotes() {
const [quotes, setQuotes] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setIsLoading(true);
fetch('https://quote-service.com/quotes')
.then(response => {
if (!response.ok)
{
lançar um novo erro('Falha ao buscar aspas');
}
retornar response.json();
})
.then(data => {
setQuotes(data);
})
.catch(err => {
setError(err.message);
})
.finally(() => {
setIsLoading(false);
});
}, []);
retorno (
<div>
{isLoading && <p>Loading...</p>}
{error && <p>Erro: {error}</p>}
<ul>
{quotes.map((quote, index) => <li key={index}>{quote}</li>)}
</ul>
</div>
);
}
O código usa o useState para gerenciar três partes do estado: quotes para armazenar as cotações,
isLoading para rastrear o status de carregamento e error para quaisquer erros de busca.
O Hook useEffect aciona a operação de busca. Se a busca for bem-sucedida, as aspas serão exibidas e
isLoading será definido como false. Se ocorrer um erro, uma mensagem de erro será exibida e
isLoading será novamente definido como false.
Como você pode observar, a parte do componente dedicada à renderização real é bem pequena (ou
seja, o código JSX dentro do retorno). Em contrapartida, o gerenciamento do estado consome quase
dois terços do corpo da função.
O uso de uma biblioteca de gerenciamento de estado de terceiros, como Redux ou MobX, pode ser
benéfico quando seu aplicativo atinge um nível de complexidade que dificulta o rastreamento de
estado. No entanto, o uso de uma biblioteca de gerenciamento de estado de terceiros tem suas
ressalvas (curva de aprendizado, práticas recomendadas em uma biblioteca específica, esforços de
migração etc.) e deve ser considerado com cuidado. É por isso que muitos desenvolvedores estão se
inclinando a usar a API de contexto integrada do React para gerenciamento de estado.
Outra complexidade significativa nos aplicativos de front-end modernos que muitas vezes passa
despercebida por muitos desenvolvedores, mas que é semelhante a um iceberg que merece mais
atenção, são os "caminhos infelizes". Vamos dar uma olhada neles a seguir.
Por exemplo, em um componente MenuItem que renderiza os dados de um item, vamos ver o que
acontece quando tentamos acessar algo que não existe no item de propriedade passado (nesse caso,
estamos procurando o apropriadamente chamado item.something.doesnt.exist):
const MenuItem =
({ item,
onItemClick,
}: {
item: MenuItemType;
onItemClick: (item: MenuItemType) => void;
}) => {
const information = item.something.doesnt.exist;
return (
<li key={item.name}>
<h3>{item.name}</h3>
<p>{item.description}</p>
<button onClick={() => onItemClick(item)}>Adicionar ao carrinho</button>
</li>
);
};
O componente MenuItem recebe um objeto de item e uma função onItemClick como props. Ele
exibe o nome e a descrição do item, além de incluir um botão Add to Cart. Quando o botão é
clicado, a função onItemClick é chamada com o item como argumento.
Isso pode causar o travamento de todo o aplicativo se não isolarmos o erro em um error boundary,
como podemos ver na Figura 1.4 - os menus não são exibidos, mas os títulos da categoria e da página
permanecem funcionais; a área afetada, que eu tracei com uma linha pontilhada vermelha, é onde os
menus deveriam aparecer. Os Error boundaries no React são um recurso que permite capturar erros de
JavaScript que ocorrem em componentes filhos, registrar esses erros e exibir uma interface de usuário
de fallback em vez de permitir que todo o aplicativo falhe. Os Error boundaries capturam erros durante
a renderização, nos métodos do ciclo de vida e nos construtores de toda a árvore abaixo deles.
Em projetos reais, sua interface do usuário pode depender de vários microsserviços ou APIs para
obter dados. Se algum desses sistemas downstream estiver inativo, sua interface do usuário deverá
levar isso em conta. Você precisará projetar fallbacks, indicadores de carregamento ou mensagens de
erro amigáveis que orientem o usuário sobre o que fazer em seguida.
Lidar com esses cenários de forma eficaz geralmente envolve a lógica de front-end e back-end, o que
acrescenta outra camada de complexidade às tarefas de desenvolvimento da interface do usuário.
Vamos examinar um componente Form básico para entender as considerações sobre a entrada do
usuário. Embora esse formulário de campo único possa exigir lógica adicional no método
handleChange, é importante observar que a maioria dos formulários geralmente consiste em vários
campos (o que significa que haverá mais comportamentos inesperados do usuário que precisamos
considerar):
Esse componente Form consiste em um único campo de entrada de texto que restringe a entrada a
caracteres alfanuméricos e espaços. Ele usa uma variável de estado de valor para armazenar o valor
do campo de entrada. A função handleChange, acionada em cada alteração de entrada, remove todos
os caracteres não alfanuméricos da entrada do usuário antes de atualizar o estado com o valor
higienizado.
A compreensão e o gerenciamento eficaz desses caminhos infelizes são essenciais para a criação de
uma interface robusta, resiliente e fácil de usar. Eles não apenas tornam seu aplicativo mais
confiável, mas também contribuem para uma experiência de usuário mais abrangente e bem pensada.
Acredito que agora você deve ter uma visão mais clara dos desafios de criar aplicativos front-end
modernos em React. Enfrentar esses obstáculos não é simples, principalmente porque o React não
oferece um guia definitivo sobre qual abordagem adotar, como estruturar sua base de código,
gerenciar estados ou garantir a legibilidade do código (e, por extensão, a facilidade de manutenção a
longo prazo) ou como os padrões estabelecidos podem ser úteis, entre outras preocupações. Essa
falta de orientação muitas vezes leva os desenvolvedores a criar soluções que podem funcionar no
curto prazo, mas que podem estar repletas de antipadrões.
Explorando antipadrões comuns no React
No âmbito do desenvolvimento de software, frequentemente encontramos práticas e abordagens que,
à primeira vista, parecem oferecer uma solução benéfica para um determinado problema. Essas
práticas, rotuladas como antipadrões, podem proporcionar alívio imediato ou uma solução
aparentemente rápida, mas geralmente escondem problemas subjacentes. Com o passar do tempo, a
dependência desses antipadrões pode levar a maiores complexidades, ineficiências ou até mesmo aos
próprios problemas que se pensava resolver.
Perfuração de adereços
Em aplicativos React complexos, gerenciar o estado e garantir que cada componente tenha acesso aos
dados de que precisa pode se tornar um desafio. Isso é frequentemente observado na forma de
perfuração de props, em que os props são passados de um componente pai por vários componentes
intermediários antes de chegarem ao componente filho que realmente precisa deles.
Por exemplo, considere uma hierarquia SearchableList, List e ListItem - uma SearchableList
contém um componente List, e List contém várias instâncias de ListItem:
Uma possível solução para evitar a perfuração de props no React é aproveitar a API Context. Ela
fornece uma maneira de compartilhar valores (dados e funções) entre componentes sem precisar
passar explicitamente as props por todos os níveis da árvore de componentes.
É comum, especialmente ao lidar com APIs ou back-ends externos, receber dados em uma forma ou
formato que não é ideal para o front-end. Em vez de ajustar esses dados em um nível superior ou em
uma função utilitária, a transformação é definida dentro do componente.
Redução da reutilização: Se outro componente exigir a mesma transformação ou uma transformação semelhante, estaremos
duplicando a lógica
Desafios de teste: O teste desse componente agora requer a consideração da lógica de transformação, tornando os testes mais
complicados
Para combater esse antipadrão, é aconselhável separar a transformação de dados do componente. Isso
pode ser feito usando funções utilitárias ou ganchos personalizados, garantindo assim um design mais
limpo e modular. Com a externalização dessas transformações, os componentes permanecem
concentrados na renderização e a lógica comercial permanece centralizada, o que resulta em uma base
de código muito mais fácil de manter.
Considere um exemplo simples. Imagine um componente destinado a exibir uma lista de itens obtidos
de uma API. Cada item tem um preço, mas queremos exibir os itens acima de um determinado limite
de preço:
Testes: O teste de unidade se torna mais complexo, pois você não está testando apenas a renderização, mas também a lógica
comercial
Manutenção: À medida que o aplicativo cresce e mais lógica é adicionada, esse componente pode se tornar pesado e mais difícil
de manter
Para garantir que nossos componentes permaneçam reutilizáveis e fáceis de manter, é aconselhável
adotar o princípio da separação de preocupações. Esse princípio afirma que cada módulo ou função
do software deve ser responsável por uma única parte da funcionalidade do aplicativo. Ao separar a
lógica de negócios da camada de apresentação e adotar uma arquitetura em camadas, podemos
garantir que cada parte do nosso código lide com sua própria responsabilidade específica, resultando
em uma base de código mais modular e de fácil manutenção.
Falta de testes
Imagine criar um componente de carrinho de compras para uma loja on-line. O carrinho é
fundamental, pois lida com adições de itens, remoções e cálculos de preço total. Por mais simples que
possa parecer, ele incorpora várias partes móveis e interconexões lógicas. Sem testes, você deixa a
porta aberta para problemas futuros, como preços incorretos, itens que não estão sendo adicionados
ou removidos corretamente ou até mesmo vulnerabilidades de segurança.
função ShoppingCart() {
const [items, setItems] = useState([]);
const addItem = (item) => {
setItems([...items, item]);
};
const removeItem = (itemId) => {
setItems(items.filter(item => item.id ! == itemId));
};
const calculateTotal = () => {
return items.reduce((total, item) => total + item.price, 0);
};
retorno (
<div>
{/* Renderize itens e controles para adicionar/remover */}
<p>Total: ${calculateTotal()}</p>
</div>
);
}
Embora a lógica desse carrinho de compras pareça simples, as possíveis armadilhas estão à espreita.
E se um item for adicionado várias vezes de forma errônea, se os preços mudarem dinamicamente ou
se forem aplicados descontos? Sem testes, esses cenários podem não ser evidentes até que o usuário
os encontre, o que pode ser prejudicial aos negócios.
Entre no desenvolvimento orientado por testes (TDD). O TDD enfatiza a criação de testes antes do
componente ou da lógica real. Para o nosso componente ShoppingCart, isso significa ter testes que
verifiquem se os itens estão corretamente
adicionados ou removidos, os cálculos totais são ajustados adequadamente e os casos extremos,
como o tratamento de descontos, são gerenciados. Somente depois que esses testes estiverem em
vigor é que a lógica real do componente deve ser implementada. O TDD é mais do que apenas
detectar erros antecipadamente; ele defende um código bem estruturado e de fácil manutenção.
Para o componente ShoppingCart, a adoção do TDD exigiria testes que garantissem que os itens
fossem adicionados ou removidos conforme o esperado, que os totais fossem calculados corretamente
e que os casos extremos fossem resolvidos sem problemas. Dessa forma, à medida que o aplicativo
cresce, os testes TDD fundamentais garantem que cada modificação ou adição mantenha a
integridade e a correção do aplicativo.
Código duplicado
É uma visão familiar em muitas bases de código: pedaços de código idêntico ou muito semelhante
espalhados por diferentes partes do aplicativo. O código duplicado não apenas incha a base de código,
mas também introduz possíveis pontos de falha. Quando um bug é detectado ou um aprimoramento é
necessário, cada instância do código duplicado pode precisar ser alterada, o que aumenta a
probabilidade de introdução de erros.
Vamos considerar dois componentes nos quais a mesma lógica de filtragem é repetida:
função AdminList(props) {
const filteredUsers = props.users.filter(user =>
user.isAdmin); return <List items={filteredUsers} />;
}
função ActiveList(props) {
const filteredUsers = props.users.filter(user => user.isActive);
return <List items={filteredUsers} />;
}
O princípio DRY (don't repeat yourself, não se repita) vem em socorro aqui. Ao centralizar a lógica
comum em funções utilitárias ou componentes de ordem superior (HOCs), o código se torna mais
fácil de manter e ler, e menos propenso a erros. Para este exemplo, poderíamos abstrair a lógica de
filtragem e reutilizá-la, garantindo uma fonte única de verdade e atualizações mais fáceis.
Imagine um componente OrderContainer que tenha uma enorme lista de objetos que inclua vários
aspectos diferentes das responsabilidades:
const OrderContainer = ({
testID,
orderData,
basketError,
addCoupon,
voucherSelected,
validationErrors,
clearErrors,
removeLine,
editLine,
hideOrderButton,
hideEditButton,
loading,
}: OrderContainerProps) => {
//..
}
Esse componente viola o princípio da responsabilidade única (SRP), que defende que um
componente deve cumprir apenas uma função. Ao assumir várias funções, ele se torna mais
complexo e menos passível de manutenção. Precisamos analisar a responsabilidade principal do
componente OrderContainer e separar a lógica de suporte em outros componentes menores e
focados ou utilizar Hooks para a separação da lógica.
OBSERVAÇÃO
Esses antipadrões listados têm variações diferentes, e discutiremos as soluções correspondentes nos capítulos
seguintes. Além disso, há também alguns princípios e padrões de design mais genéricos que discutiremos no livro,
bem como algumas práticas de engenharia comprovadas, como refatoração e TDD.
Enquanto isso, a programação orientada por interface, em sua essência, concentra-se na adaptação
do software em torno das interações que ocorrem entre os módulos de software, predominantemente
por meio de interfaces. Esse modus operandi promove a agilidade, tornando os módulos de software
não apenas mais coerentes, mas também passíveis de alterações. O paradigma dos componentes sem
cabeça, por outro lado, incorpora componentes que, embora não tenham funções diretas de
renderização, são encarregados do gerenciamento do estado ou da lógica. Esses componentes passam
o bastão para suas contrapartes consumidoras para renderização da interface do usuário, defendendo
assim a adaptabilidade e a reutilização.
Ao obter uma compreensão firme desses padrões de design e implantá-los criteriosamente, estamos
posicionados para contornar os erros predominantes, elevando assim a estatura de nossos aplicativos
React.
Além disso, no ecossistema de codificação, os pilares gêmeos do TDD e da refatoração consistente
surgem como ferramentas formidáveis para acentuar a qualidade do código. O TDD, com seu
chamado de teste antes do código, fornece um ciclo de feedback imediato para possíveis
discrepâncias. Junto com o TDD, o ethos da refatoração persistente garante que o código seja
perpetuamente otimizado e aperfeiçoado. Essas metodologias não apenas definem o padrão de
excelência do código, mas também instilam a adaptabilidade a mudanças futuras.
Resumo
Neste capítulo, exploramos os desafios do desenvolvimento da interface do usuário, desde suas
complexidades até os problemas de gerenciamento de estado. Também discutimos os antipadrões
comuns devido à natureza de sua complexidade e apresentamos brevemente nossa abordagem que
combina práticas recomendadas e estratégias de teste eficazes. Isso estabelece a base para um
desenvolvimento de front-end mais eficiente e robusto.
No próximo capítulo, vamos nos aprofundar nos fundamentos do React, fornecendo a você as
ferramentas e o conhecimento necessários para dominar essa poderosa biblioteca. Fique ligado!