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:
- Necessidade de estrutura desnecessária de elementos filho
- 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:
- Um componente pode ter muitas estruturas de dados; o layout permanece o mesmo
- Ao receber dados, eles são processados de acordo com o cenário desejado.
- 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:
- Ao adicionar uma camada de lógica, finalmente perdemos a conexão direta entre os dados e a exibição
- 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
- 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 ):
- Desenvolvimento de front-end - desenvolvimento de interfaces de usuário - usa HTML para layout
- 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ê:
- Ambos os adereços são necessários. Isso limita a reutilização de componentes
- Opcional significaria sobrecarregar o componente SplitPane com lógica.
- Aninhamento e pluralidade não são exibidos muito semanticamente.
- 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.