Problemas dos padrões básicos para criar aplicativos orientados a dados no React.JS

Para criar interfaces, o React recomenda o uso de bibliotecas de composição e gerenciamento de estado para criar hierarquias de componentes. No entanto, com padrões complexos de composição, os problemas aparecem:


  1. Necessidade de estrutura desnecessária de elementos filho
  2. Ou passe-os como adereços, o que complica a legibilidade, a semântica e a estrutura do código

Para a maioria dos desenvolvedores, o problema pode não ser óbvio e o coloca no nível de gerenciamento de estado. Isso também é discutido na documentação do React:


Se você deseja se livrar de passar alguns adereços para muitos níveis, geralmente a composição do componente é uma solução mais simples que o contexto.
Reagir documentação Contexto.

Se seguirmos o link, veremos outro argumento:


No Facebook, usamos o React em milhares de componentes e não encontramos nenhum caso em que recomendamos a criação de hierarquias de herança de componentes.
Reagir documentação Composição versus herança.

Obviamente, se todos que usam a ferramenta leem a documentação e reconhecem a autoridade dos autores, esta publicação não seria. Portanto, analisamos os problemas das abordagens existentes.


Padrão 1 - Componentes Controlados Diretamente



Comecei com esta solução por causa da estrutura do Vue que recomenda essa abordagem . Pegamos a estrutura de dados que vem do suporte ou do design no caso do formulário. Encaminhar para o nosso componente - por exemplo, para uma placa de filme:


const MovieCard = (props) => { const {title, genre, description, rating, image} = props.data; return ( <div> <img href={image} /> <div><h2>{title}</h2></div> <div><p>{genre}</p></div> <div><p>{description}</p></div> <div><h1>{rating}</h1></div> </div> ) } 

Parar Já sabemos sobre a expansão sem fim dos requisitos de componentes. De repente, o título terá um link para a resenha do filme? E o gênero - para os melhores filmes dele? Não adicione agora:


 const MovieCard = (props) => { const {title: {title}, description: {description}, rating: {rating}, genre: {genre}, image: {imageHref} } = props.data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> <div><h1>{rating}</h1></div> <div><p>{description}</p></div> </div> ) } 

Portanto, nos protegeremos de problemas no futuro, mas abriremos a porta para um erro de ponto zero. Inicialmente, poderíamos lançar estruturas diretamente na parte de trás:


 <MovieCard data={res.data} /> 

Agora, toda vez que você precisar duplicar todas as informações:


 <MovieCard data={{ title: {res.title}, description: {res.description}, rating: {res.rating}, image: {res.imageHref} }} /> 

No entanto, esquecemos o gênero - e o componente caiu. E se os limitadores de erro não foram configurados, todo o aplicativo está com ele.


TypeScript vem em socorro. Simplificamos o esquema refatorando o cartão e os elementos que o utilizam. Felizmente, tudo é destacado no editor ou durante a montagem:


 interface IMovieCardElement { text?: string; } interface IMovieCardImage { imageHref?: string; } interface IMovieCardProps { title: IMovieCardElement; description: IMovieCardElement; rating: IMovieCardElement; genre: IMovieCardElement; image: IMovieCardImage; } ... const {title: {text: title}, description: {text: description}, rating: {text: rating}, genre: {text: genre}, image: {imageHref} } = props.data; 

Para economizar tempo, ainda rolamos os dados "como qualquer" ou "como IMovieCardProps". O que acontece? Já descrevemos três vezes (se usadas em um local) uma estrutura de dados. E o que nós temos? Um componente que ainda não pode ser modificado. Um componente que pode potencialmente travar o aplicativo inteiro.


É hora de reutilizar este componente. A classificação não é mais necessária. Temos duas opções:


Coloque prop semRating sempre que a classificação for necessária


 const MovieCard = ({withoutRating, ...props}) => { const {title: {title}, description: {description}, rating: {rating}, genre: {genre}, image: {imageHref} } = props.data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> { withoutRating && <div><h1>{rating}</h1></div> } <div><p>{description}</p></div> </div> ) } 

Rápido, mas empilhamos objetos e construímos uma quarta estrutura de dados.


Tornando a classificação no IMovieCardProps opcional. Não se esqueça de torná-lo um objeto vazio por padrão


 const MovieCard = ({data, ...props}) => { const {title: {text: title}, description: {text: description}, rating: {text: rating} = {}, genre: {text: genre}, image: {imageHref} } = data; return ( <div> <img href={imageHref} /> <div><h2>{name}</h2></div> <div><p>{genre}</p></div> { data.rating && <div><h1>{rating}</h1></div> } <div><p>{description}</p></div> </div> ) } 

Mais complicado, mas fica difícil ler o código. Mais uma vez, repita pela quarta vez . O controle sobre o componente não é óbvio, pois é controlado de maneira opaca pela estrutura de dados. Digamos que nos pediram para transformar a notória classificação em um link, mas não em todos os lugares:


  rating: {text: rating, url: ratingUrl} = {}, ... { data.rating && data.rating.url ? <div>><h1><a href={ratingUrl}{rating}</a></h1></div> : <div><h1>{rating}</h1></div> } 

E aqui nos deparamos com a lógica complexa que dita a estrutura de dados opaca.


Padrão No. 2 - Componentes com seu próprio estado e redutores



Ao mesmo tempo, uma abordagem estranha e popular. Eu o usei quando comecei a trabalhar com o React e a funcionalidade JSX no Vue estava ausente. Mais de uma vez, ouvi dos desenvolvedores da mitaps que essa abordagem permite ignorar estruturas de dados mais generalizadas:


  1. Um componente pode ter muitas estruturas de dados; o layout permanece o mesmo
  2. Ao receber dados, eles são processados ​​de acordo com o cenário desejado.
  3. Os dados são armazenados no estado do componente para não iniciar o redutor a cada renderização (opcional)

Naturalmente, o problema de opacidade (1) é complementado pelo problema de sobrecarga lógica (2) e pela adição de estado ao componente final (3).


O último (3) é ditado pela segurança interna do objeto. Ou seja, verificamos profundamente os objetos por meio do lodash.isEqual. Se o evento for avançado ou JSON.stringify, tudo está apenas começando. Você também pode adicionar um carimbo de data e hora e verificar se tudo está perdido. Não há necessidade de salvar ou memorizar, porque a otimização pode ser mais complicada do que um redutor devido à complexidade do cálculo.


Os dados são lançados com o nome do script (geralmente uma string):


 <MovieCard data={{ type: 'withoutRating', data: res.data, }} /> 

Agora escreva o componente:


 const MovieCard = ({data}) => { const card = reduceData(data.type, data.data); return ( <div> <img href={card.imageHref} /> <div><h2>{card.name}</h2></div> <div><p>{card.genre}</p></div> { card.withoutRating && <div><h1>{card.rating}</h1></div> } <div><p>{card.description}</p></div> </div> ) } 

E a lógica:


 const reduceData = (type, data) = { switch (type) { case 'withoutRating': return { title: {data.title}, description: {data.description}, rating: {data.rating}, genre: {data.genre}, image: {data.imageHref} withoutRating: true, }; ... } }; 

Existem vários problemas nesta etapa:


  1. Ao adicionar uma camada de lógica, finalmente perdemos a conexão direta entre os dados e a exibição
  2. A duplicação da lógica para cada caso significa que, no caso em que todos os cartões precisem de uma classificação etária, ele precisará ser registrado em cada redutor
  3. Outros problemas restantes da etapa 1

Padrão 3 - Transferindo Lógica e Dados de Exibição para Gerenciamento de Estado



Aqui abandonamos o barramento de dados para criar interfaces, que é React. Utilizamos tecnologia com nosso próprio modelo lógico. Essa é provavelmente a maneira mais comum de criar aplicativos no React, embora o guia o avise para não usar o contexto dessa maneira.


Use ferramentas semelhantes nas quais o React não fornece ferramentas suficientes - por exemplo, no roteamento. Provavelmente você está usando o react-router. Nesse caso, usar o contexto em vez de encaminhar o retorno de chamada do componente de cada rota de nível superior faria mais sentido encaminhar a sessão para todas as páginas. O React não possui uma abstração separada para ações assíncronas que não sejam as oferecidas pela linguagem Javascript.


Parece que há uma vantagem: podemos reutilizar a lógica em versões futuras do aplicativo. Mas isso é uma farsa. Por um lado, está vinculado à API, por outro, à estrutura do aplicativo, e a lógica fornece essa conexão. Ao alterar uma das peças, ela precisa ser reescrita.


Solução: Padrão nº 4 - composição



O método de composição é óbvio se você seguir os seguintes princípios (além de uma abordagem semelhante em Design Patterns ):


  1. Desenvolvimento de front-end - desenvolvimento de interfaces de usuário - usa HTML para layout
  2. Javascript é usado para receber, transmitir e processar dados.

Portanto, transfira os dados de um domínio para outro o mais cedo possível. O React usa a abstração JSX para modelar o HTML, mas na verdade ele usa um conjunto de métodos createElement. Ou seja, os componentes JSX e React, que também são elementos JSX, devem ser tratados como um método de exibição e comportamento, em vez de transformação e processamento de dados que devem ocorrer em um nível separado.


Nesta etapa, muitos usam os métodos listados acima, mas não solucionam o principal problema de expandir e modificar a exibição dos componentes. Como fazer isso, de acordo com os criadores da biblioteca, é mostrado na documentação:


 function SplitPane(props) { return ( <div className="SplitPane"> <div className="SplitPane-left"> {props.left} </div> <div className="SplitPane-right"> {props.right} </div> </div> ); } function App() { return ( <SplitPane left={ <Contacts /> } right={ <Chat /> } /> ); } 

Ou seja, como parâmetros, em vez de cadeias, números e tipos booleanos, componentes prontos e confeccionados são transferidos.


Infelizmente, esse método também se mostrou inflexível. Aqui está o porquê:


  1. Ambos os adereços são necessários. Isso limita a reutilização de componentes
  2. Opcional significaria sobrecarregar o componente SplitPane com lógica.
  3. Aninhamento e pluralidade não são exibidos muito semanticamente.
  4. Essa lógica de mapeamento teria que ser reescrita para cada componente que aceita adereços.

Como resultado, essa solução pode crescer em complexidade, mesmo em cenários bastante simples:


 function SplitPane(props) { return ( <div className="SplitPane"> { props.left && <div className="SplitPane-left"> {props.left} </div> } { props.right && <div className="SplitPane-right"> {props.right} </div> } </div> ); } function App() { return ( <SplitPane left={ contacts.map(el => <Contacts name={ <ContactsName name={el.name} /> } phone={ <ContactsPhone name={el.phone} /> } /> ) } right={ <Chat /> } /> ); } 

Na documentação, um código semelhante, no caso de componentes de ordem superior (HOC) e objetos de renderização, é chamado de " inferno de invólucros". A cada adição de um novo elemento, a legibilidade do código se torna mais difícil.


Uma resposta para esse problema - slots - está presente na tecnologia de componentes da Web e na estrutura do Vue . Nos dois lugares, no entanto, existem restrições: primeiro, os slots são definidos não por um símbolo, mas por uma string, o que complica a refatoração. Em segundo lugar, os slots têm funcionalidade limitada e não podem controlar sua própria exibição, transferir outros slots para componentes filhos ou ser reutilizados em outros elementos.


Em resumo, algo como isso, vamos chamá-lo de padrão número 5 - slots :


 function App() { return ( <SplitPane> <LeftPane> <Contacts /> </LeftPane> <RightPane> <Chat /> </RightPane> </SplitPane> ); } 

Falarei sobre isso no próximo artigo sobre soluções existentes para o padrão de slot no React e sobre minha própria solução.

Source: https://habr.com/ru/post/pt475170/


All Articles