Problèmes des modèles de base pour la création d'applications pilotées par les données sur React.JS

Pour créer des interfaces, React recommande d' utiliser des bibliothèques de gestion des compositions et des états pour créer des hiérarchies de composants. Cependant, avec des motifs de composition complexes, des problèmes apparaissent:


  1. Besoin de structurer inutilement les éléments enfants
  2. Ou passez-les comme accessoires, ce qui complique la lisibilité, la sémantique et la structure du code

Pour la plupart des développeurs, le problème peut ne pas être évident, et ils le jettent au niveau de la gestion des états. Ceci est également abordé dans la documentation React:


Si vous voulez vous débarrasser de la transmission de certains accessoires à plusieurs niveaux, la composition des composants est généralement une solution plus simple que le contexte.
Documentation React Contexte.

Si nous suivons le lien, nous verrons un autre argument:


Sur Facebook, nous utilisons React dans des milliers de composants, et nous n'avons trouvé aucun cas où nous recommanderions de créer des hiérarchies d'héritage de composants.
Documentation React Composition versus héritage.

Bien sûr, si tous ceux qui utilisent l'outil lisent la documentation et reconnaissent l'autorité des auteurs, cette publication ne l'aurait pas été. Par conséquent, nous analysons les problèmes des approches existantes.


Modèle n ° 1 - Composants directement contrôlés



J'ai commencé avec cette solution en raison du cadre Vue qui recommande cette approche . Nous prenons la structure de données qui provient du support ou de la conception dans le cas du formulaire. Transférer à notre composant - par exemple, sur une carte de film:


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> ) } 

Arrêter Nous connaissons déjà l'expansion sans fin des exigences en matière de composants. Soudain, le titre aura un lien vers la critique du film? Et le genre - pour les meilleurs films? N'ajoutez pas maintenant:


 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> ) } 

Nous allons donc nous protéger des problèmes à l'avenir, mais ouvrir la porte à une erreur de point zéro. Au départ, nous pouvions lancer des structures directement par l'arrière:


 <MovieCard data={res.data} /> 

Maintenant, chaque fois que vous devez dupliquer toutes les informations:


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

Cependant, nous avons oublié le genre - et le composant est tombé. Et si les limiteurs d'erreur n'étaient pas définis, alors l'application entière est avec.


TypeScript vient à la rescousse. Nous simplifions le schéma en refactorisant la carte et les éléments qui l'utilisent. Heureusement, tout est mis en évidence dans l'éditeur ou lors du montage:


 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; 

Pour gagner du temps, nous continuons à rouler les données «comme n'importe quel» ou «comme IMovieCardProps». Qu'est-ce qui se passe? Nous avons déjà décrit trois fois (si utilisé en un seul endroit) une structure de données. Et qu'avons-nous? Un composant qui ne peut toujours pas être modifié. Un composant qui pourrait potentiellement planter l'application entière.


Il est temps de réutiliser ce composant. L'évaluation n'est plus nécessaire. Nous avons deux options:


Mettez l'hélice sans évaluation partout où une évaluation est nécessaire


 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> ) } 

Rapide, mais nous empilons des accessoires et construisons une quatrième structure de données.


Rendre la notation dans IMovieCardProps facultative. N'oubliez pas d'en faire un objet vide par défaut


 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> ) } 

Plus délicat, mais il devient difficile de lire le code. Encore une fois, répétez pour la quatrième fois . Le contrôle du composant n'est pas évident, car il est contrôlé de manière opaque par la structure des données. Disons qu'on nous a demandé de faire de la notoire note un lien, mais pas partout:


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

Et ici, nous rencontrons la logique complexe qui dicte la structure de données opaque.


Schéma n ° 2 - Composants avec leur propre état et réducteurs



Dans le même temps, une approche étrange et populaire. Je l'ai utilisé lorsque j'ai commencé à travailler avec React et que la fonctionnalité JSX de Vue faisait défaut. Plus d'une fois, j'ai entendu des développeurs de mitaps dire que cette approche vous permet d'ignorer des structures de données plus généralisées:


  1. Un composant peut prendre de nombreuses structures de données; la disposition reste la même
  2. Lors de la réception de données, il les traite selon le scénario souhaité.
  3. Les données sont stockées dans l'état du composant afin de ne pas démarrer le réducteur à chaque rendu (facultatif)

Naturellement, le problème d'opacité (1) est complété par le problème de surcharge logique (2) et de l'ajout d'état au composant final (3).


Le dernier (3) est dicté par la sécurité interne de l'objet. Autrement dit, nous vérifions en profondeur les objets via lodash.isEqual. Si l'événement est avancé ou JSON.stringify, tout ne fait que commencer. Vous pouvez également ajouter un horodatage et vérifier si tout est perdu. Il n'est pas nécessaire de sauvegarder ou de mémoriser, car l'optimisation peut être plus compliquée qu'un réducteur en raison de la complexité du calcul.


Les données sont lancées avec le nom du script (généralement une chaîne):


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

Maintenant écrivez le composant:


 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> ) } 

Et la logique:


 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, }; ... } }; 

Il y a plusieurs problèmes dans cette étape:


  1. En ajoutant une couche de logique, nous perdons enfin la connexion directe entre les données et l'affichage
  2. La duplication de la logique pour chaque cas signifie que dans le cas où toutes les cartes ont besoin d'une classification par âge, elle devra être enregistrée dans chaque réducteur
  3. Autres problèmes restants de l'étape 1

Modèle n ° 3 - Transfert de la logique d'affichage et des données vers la gestion des états



Ici, nous abandonnons le bus de données pour créer des interfaces, qui est React. Nous utilisons la technologie avec notre propre modèle logique. C'est probablement la façon la plus courante de créer des applications sur React, bien que le guide vous avertisse de ne pas utiliser le contexte de cette manière.


Utilisez des outils similaires lorsque React ne fournit pas suffisamment d'outils, par exemple pour le routage. Il est fort probable que vous utilisiez react-router. Dans ce cas, l'utilisation du contexte au lieu de transférer le rappel à partir du composant de chaque route de niveau supérieur aurait plus de sens pour transférer la session vers toutes les pages. React n'a pas d'abstraction distincte pour les actions asynchrones autres que celles proposées par le langage Javascript.


Il semblerait qu'il y ait un plus: on peut réutiliser la logique dans les futures versions de l'application. Mais c'est un canular. D'une part, elle est liée à l'API, d'autre part, à la structure de l'application, et la logique fournit cette connexion. Lors du changement d'une des pièces, elle doit être réécrite.


Solution: motif n ° 4 - composition



La méthode de composition est évidente si vous suivez les principes suivants (à part une approche similaire dans Design Patterns ):


  1. Développement frontend - développement d'interfaces utilisateur - utilise HTML pour la mise en page
  2. Javascript est utilisé pour recevoir, transmettre et traiter des données.

Par conséquent, transférez les données d'un domaine à un autre le plus tôt possible. React utilise l'abstraction JSX pour créer un modèle HTML, mais en fait, il utilise un ensemble de méthodes createElement. En d'autres termes, les composants JSX et React, qui sont également des éléments JSX, doivent être traités comme une méthode d'affichage et de comportement, plutôt que comme une transformation et un traitement des données qui doivent se produire à un niveau distinct.


À cette étape, beaucoup utilisent les méthodes répertoriées ci-dessus, mais elles ne résolvent pas le problème clé de l'extension et de la modification de l'affichage des composants. Comment le faire selon les créateurs de la bibliothèque est indiqué dans la documentation:


 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 /> } /> ); } 

Autrement dit, en tant que paramètres, au lieu de chaînes, de nombres et de types booléens, des composants prêts à l'emploi et composés sont transférés.


Hélas, cette méthode s'est également avérée inflexible. Voici pourquoi:


  1. Les deux accessoires sont requis. Cela limite la réutilisation des composants
  2. Facultatif signifierait surcharger le composant SplitPane avec la logique.
  3. L'imbrication et la pluralité ne sont pas affichées de façon très sémantique.
  4. Cette logique de mappage devrait être réécrite pour chaque composant qui accepte les accessoires.

En conséquence, une telle solution peut croître en complexité même pour des scénarios assez 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 /> } /> ); } 

Dans la documentation, un code similaire, dans le cas des composants d'ordre supérieur (HOC) et des accessoires de rendu, est appelé un « enfer d'enveloppe». Avec chaque ajout d'un nouvel élément, la lisibilité du code devient plus difficile.


Une réponse à ce problème - les slots - est présente dans la technologie des composants Web et dans le framework Vue . Dans les deux cas, cependant, il y a des restrictions: premièrement, les emplacements ne sont pas définis par un symbole, mais par une chaîne, ce qui complique le refactoring. Deuxièmement, les emplacements sont limités en fonctionnalités et ne peuvent pas contrôler leur propre affichage, transférer d'autres emplacements vers des composants enfants ou être réutilisés dans d'autres éléments.


En bref, quelque chose comme ça, appelons-le modèle n ° 5 - slots :


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

J'en parlerai dans le prochain article sur les solutions existantes pour le modèle de slot dans React et sur ma propre solution.

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


All Articles