Juntao Qiu React Anti Patterns Build Efficient and Maintainable React Applications With Test Driven 30 46 PT

Fazer download em pdf ou txt
Fazer download em pdf ou txt
Você está na página 1de 19

Assine o DeepL Pro para traduzir arquivos maiores.

Mais informações em www.DeepL.com/pro.

Apresentando os antipadrões do React


Este livro mergulha profundamente no reino dos antipadrões do React. Um antipadrão não é
necessariamente um erro técnico - o código geralmente funciona corretamente no início - mas,
embora inicialmente pareça correto, à medida que a base de código se expande, esses antipadrões
podem se tornar problemáticos.

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.

Meu objetivo é a praticidade. As ilustrações de código são originárias de projetos anteriores ou de


domínios comuns, como um carrinho de compras e um componente de perfil de usuário,
minimizando a necessidade de decifrar o jargão do domínio. Para uma visão holística, os capítulos
finais apresentam exemplos detalhados de ponta a ponta, proporcionando uma experiência mais
organizada e envolvente.

Especificamente, neste capítulo introdutório, abordaremos os meandros da construção de aplicativos


React avançados, destacando como o gerenciamento de estado e as operações assíncronas podem
ofuscar a clareza do código. Enumeraremos os antipadrões predominantes e daremos uma olhada nas
estratégias corretivas detalhadas mais adiante no livro.

Neste capítulo, abordaremos os seguintes tópicos:


Compreender a dificuldade de criar interfaces de usuário

Entendendo o gerenciamento de estado

Explorando "caminhos infelizes"

Explorando antipadrões comuns no React

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.

Compreender a dificuldade de criar interfaces de usuário


A menos que você esteja criando uma página da Web simples e semelhante a um documento - por
exemplo, um artigo básico sem elementos avançados de interface do usuário, como caixas de
pesquisa ou modais - as linguagens integradas oferecidas pelos navegadores da Web geralmente são
insuficientes. A Figura 1.1 mostra um exemplo de um site que usa HTML (HyperText Markup
Language):

Figura 1.1: Um site de documento HTML simples

No entanto, hoje em dia, a maioria dos aplicativos é mais complicada e contém mais elementos do
que os originalmente previstos para essa linguagem.

A disparidade entre a linguagem da Web e as experiências de interface do usuário que as pessoas


encontram diariamente é substancial. Seja uma plataforma de reserva de passagens, uma ferramenta de
gerenciamento de projetos ou uma galeria de imagens, as interfaces de usuário modernas da Web são
complexas e as linguagens nativas da Web não as suportam prontamente. Você pode fazer um esforço
extra para "simular" componentes de IU, como acordeões, botões de alternância ou cartões interativos,
mas, fundamentalmente, ainda estará trabalhando com o que equivale a um documento, não com um
componente de IU genuíno.

Em um mundo ideal, a criação de uma IU se assemelharia ao trabalho com um designer de IU visual.


Ferramentas como o C++ Builder ou o Delphi, ou alternativas mais modernas como o Figma,
permitem arrastar e soltar componentes em uma tela que é renderizada sem problemas em qualquer
tela. Esse não é o caso da Web
desenvolvimento. Por exemplo, para criar uma entrada de pesquisa personalizada, será necessário
envolvê-la em elementos adicionais, ajustar as cores, ajustar o preenchimento e as fontes e, talvez,
adicionar um ícone para orientação do usuário. A criação de uma lista de sugestões automáticas que
apareça logo abaixo da caixa de pesquisa, correspondendo exatamente à sua largura, costuma ser
muito mais trabalhosa do que se imagina inicialmente.

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:

Figura 1.2: Visualização de problemas do Jira

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.

Entendendo o gerenciamento de estado


Gerenciar o estado no desenvolvimento de front-end moderno é uma tarefa complexa. Quase todos os
aplicativos precisam recuperar dados de um servidor remoto por meio de uma rede - podemos chamar
esses dados de estados remotos. O estado remoto se origina de uma fonte externa, normalmente um
servidor backend ou uma API. Isso contrasta com o estado local, que é gerado e gerenciado
inteiramente dentro do próprio aplicativo de front-end.

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.

E esses são apenas alguns dos desafios dos estados remotos.

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.

Se as citações forem de um servidor remoto, o código se transformará em algo como o seguinte:

import React, { useState, useEffect } from 'react';


function Quotes() {
const [quotes, setQuotes] = useState<string[]>([]);
useEffect(() => {
fetch('https://quote-service.com/quotes')
.then(response => response.json())
.then(data => setQuotes(data));
}, []);
retorno (
<ul>
{quotes.map((quote, index) => <li key={index}>{quote}</li>)}
</ul>
);
}
exportar cotações padrão;

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.

Mas esse é apenas um aspecto do gerenciamento de estado. Há também a questão do gerenciamento


do estado local, o que significa que o estado só precisa ser mantido dentro de um componente. Por
exemplo, conforme demonstrado na Figura 1.3, um componente acordeão precisa rastrear se está
expandido ou recolhido - quando você clica no triângulo no cabeçalho, ele alterna o painel de
listagem:
Figura 1.3: Uma seção expansível

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.

Explorando "caminhos infelizes"


Quando se trata de desenvolvimento de interface do usuário, nosso foco principal costuma ser o
"caminho feliz", ou seja, a jornada ideal do usuário em que tudo sai como planejado. No entanto,
negligenciar os "caminhos infelizes" pode tornar sua interface do usuário muito mais complicada do
que você imagina inicialmente. Aqui estão alguns cenários que podem levar a caminhos infelizes e,
consequentemente, complicar seus esforços de desenvolvimento da interface do usuário.
Erros gerados por outros componentes
Imagine que você esteja usando um componente de terceiros ou até mesmo um componente de outra
equipe em seu aplicativo. Se esse componente lançar um erro, ele poderá quebrar a interface do usuário
ou levar a comportamentos inesperados que você terá de levar em conta. Isso pode envolver a adição
de lógica condicional ou limites de erro para lidar com esses erros de forma elegante, tornando a
interface do usuário mais complexa do que o previsto inicialmente.

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.

Esse código tenta acessar uma propriedade inexistente, item.something.doesnt.exist, o que


causará um erro de tempo de execução. Conforme demonstrado na Figura 1.4, o aplicativo parou
de funcionar depois que o serviço de backend retornou alguns dados inesperados:
Figura 1.4: Uma exceção lançada por um componente durante a renderização

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.

Aprendendo o comportamento inesperado do usuário


Independentemente da perfeição com que você projeta a interface do usuário, os usuários sempre
encontrarão maneiras de usar o sistema de uma forma que você não previu. Se eles inserirem
caracteres especiais em campos de texto, tentarem enviar formulários com muita rapidez ou usarem
extensões de navegador que interfiram no seu site, você terá de projetar a interface do usuário para
lidar com esses casos extremos. Isso significa implementar validação, verificações e proteções
adicionais que podem complicar a base de código 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):

import React, { ChangeEvent, useState } from "react";


const Form = () => {
const [value, setValue] = useState<string>("");
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const inputValue = event.target.value;
const sanitizedValue = inputValue.replace(/[^\w\s]/gi, "");
setValue(sanitizedValue);
};
retorno (
<div>
<form>
<label>
Entrada sem caracteres especiais:
<input type="text" value={value} onChange={handleChange} />
</label>
</form>
</div>
);
};
exportar o formulário padrão;

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.

Reconhecer e compreender esses antipadrões é fundamental para os desenvolvedores, pois permite


que eles antecipem possíveis armadilhas e evitem soluções que podem ser contraproducentes no
longo prazo. Nas próximas seções, destacaremos os antipadrões comuns acompanhados de exemplos
de código.
Abordaremos cada antipadrão e descreveremos as possíveis soluções. No entanto, não vamos nos
aprofundar aqui, pois capítulos inteiros são dedicados a discutir esses tópicos em detalhes.

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:

function SearchableList({ items, onItemClick }) {


return (
<div className="searchable-list">
(*) Potencialmente alguma funcionalidade de pesquisa aqui */}
<List items={items} onItemClick={onItemClick} />
</div>
);
}
function List({ items, onItemClick }) {
return (
<ul className="list">
{items.map(item => (
<ListItem key={item.id} data={item} onItemClick={onItemClick}
/>
))}
</ul>
);
}
function ListItem({ data, onItemClick }) {
return (
<li className="list-item" onClick={() => onItemClick(data.id)}>
{data.name}
</li>
);
}
Nessa configuração, a prop de onItemClick é perfurada de SearchableList para List e, finalmente,
para
ListItem. Embora o componente List não use essa prop, ele precisa passá-la para ListItem.

Essa abordagem pode levar ao aumento da complexidade e à redução da capacidade de manutenção.


Quando várias props são transmitidas por vários componentes, pode ser difícil entender o fluxo de
dados e fazer a depuração.

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.

Transformação de dados no componente


A abordagem centrada em componentes no React tem tudo a ver com a divisão de tarefas e
preocupações em partes gerenciáveis, melhorando a capacidade de manutenção. No entanto, um
passo em falso recorrente é quando os desenvolvedores introduzem uma lógica complexa de
transformação de dados diretamente nos 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.

Considere o seguinte cenário:

function UserProfile({ userId }) {


const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
// Transformando dados diretamente dentro do
componente const transformedUser = {
name: `${data.firstName} ${data.lastName}`,
age: data.age,
address: `${data.addressLine1}, ${data.city}, ${data.
country}`
};
setUser(transformedUser);
});
},
[userId]);
return (
<div>
{user && (
<>
<p>Nome: {user.name}</p>
<p>Age: {user.age}</p>
<p>Endereço: {user.address}</p>
</>
)}
</div>
);
}
O componente de função UserProfile recupera e exibe o perfil de um usuário com base no prop
userId fornecido. Depois que os dados remotos são obtidos, eles são transformados no próprio
componente para criar um perfil de usuário estruturado. Esses dados transformados consistem no
nome completo do usuário (uma combinação de nome e sobrenome), idade e um endereço formatado.

Ao incorporar diretamente a transformação, encontramos alguns problemas:


Falta de clareza: A combinação de tarefas de obtenção, transformação e renderização de dados em um único componente
dificulta a identificação da finalidade exata 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.

Lógica complicada em exibições


A beleza das estruturas de front-end modernas, incluindo o React, é a separação distinta de
preocupações. Por design, os componentes devem ser alheios às complexidades da lógica comercial,
concentrando-se, em vez disso, na apresentação. No entanto, uma armadilha recorrente que os
desenvolvedores encontram é a infusão da lógica comercial nos componentes de visualização. Isso
não só atrapalha a separação limpa, mas também incha os componentes e os torna mais difíceis de
testar e reutilizar.

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:

função PriceListView({ items }) {


// Lógica de negócios dentro da
visualização
const filterExpensiveItems = (items) => {
return items.filter(item => item.price > 100);
}
const expensiveItems = filterExpensiveItems(items);
return (
<div>
{expensiveItems.map(item => (
<div key={item.id}>
{item.name}: ${item.price}
</div>
))}
</div>
);
}

Aqui, a função filterExpensiveItems, uma parte da lógica comercial, reside diretamente no


componente de visualização. O componente agora tem a tarefa de não apenas apresentar dados, mas
também de processá-los.
Essa abordagem pode se tornar problemática:
Reutilização: Se outro componente exigir um filtro semelhante, a lógica precisará ser duplicada

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.

Considere esta versão simplista de um carrinho de compras:

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.

Componente longo com muita responsabilidade


O React incentiva a criação de componentes modulares e reutilizáveis. No entanto, à medida que os
recursos são adicionados, um componente pode crescer rapidamente em tamanho e responsabilidade,
transformando-se em um gigante pesado. Um componente longo que gerencia várias tarefas torna-se
difícil de manter, entender e testar.

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.

Revelando nossa abordagem para demolir antipadrões


Quando se trata de abordar os antipadrões predominantes, um arsenal de padrões de design vem à
tona. Técnicas como render props, HOCs e Hooks são fundamentais para aumentar os recursos dos
componentes sem se desviar de suas funções principais, enquanto o aproveitamento de padrões
fundamentais, como arquitetura em camadas e separação de preocupações, garante uma base de
código simplificada, demarcando lógica, dados e apresentação de forma coerente. Essas práticas não
apenas elevam a sustentabilidade dos aplicativos React, mas também estabelecem as bases para um
trabalho em equipe eficaz entre os desenvolvedores.

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.

À medida que navegamos no reino da refatoração, é fundamental mergulhar na essência dessas


técnicas, discernindo suas complexidades e pontos de aplicação ideais. O aproveitamento dessas vias
de refatoração promete aumentar a clareza, a sustentabilidade e a eficiência geral de seu código. Isso
é algo que faremos ao longo do livro!

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!

Você também pode gostar