Problèmes de division fine des composants et de propriété de l’État
Après avoir découpé un état en plusieurs vérités locales, la séquence devient un événement probabiliste
Les symptômes de ce bug en ligne sont très similaires à ceux « occasionnels », mais ils ne sont pas aléatoires.
Sur la même page, un état métier existera sous différentes formes dans différents composants : paramètres d’URL, état du composant parent, état local du composant enfant, cache renvoyé par la requête, et même valeurs dérivées calculées par un certain sélecteur. Plus les composantes sont finement décomposées, plus ces « vérités partielles » deviennent nombreuses. Tant que « qui peut écrire, qui sait et qui est responsable du timing » ne converge pas d’abord vers une seule règle, la cause première de l’état d’erreur en ligne passera de « un certain morceau de code est mal écrit » à « plusieurs morceaux de code sont écrits correctement, mais l’ordre d’écriture est instable ».
La chose la plus difficile dans ce type de problème est le dépannage et non la réparation. Parce qu’il semble que chaque composant soit très raisonnable : ils gèrent leur propre petit état, leur mise en cache et leur propre chargement. Mais une fois combiné, le système n’a pas de propriétaire d’état unique, et la performance finale est la suivante : il suffit d’actualiser, de changer d’onglet et de réessayer. Si vous souhaitez utiliser les journaux pour connecter des liens, vous constaterez que le même champ provient des accessoires, du cache local et des paquets de retour de requête. Qui couvre qui dépend entièrement du rythme de rendu et du délai de demande.
Le jugement de cet article est simple :
Si la division des composants ne converge pas d’abord vers “qui écrit l’état, qui connaît les détails et qui est responsable du timing”, le même état sera découpé en plusieurs vérités partielles et la séquence de mise à jour deviendra un événement probabiliste. Enfin, les avantages de la réutilisation seront remplacés par des états d’erreur occasionnels, et le coût du rendu et du dépannage répétés se produira.
Ci-dessous, j’utiliserai un dépannage d’état d’erreur en ligne très typique pour expliquer comment le faire converger étape par étape.
Scène : Un état d’erreur “occasionnel”
La page est une liste + une barre de filtre supérieure.
- Il y a une requête :
?tab=all&sort=latest&city=shsur l’URL - La barre de filtre supérieure est divisée en plusieurs petits composants : onglet, tri, liste déroulante Ville
- Le composant liste crée un cache du “résultat de la dernière requête” pour éviter le scintillement lors du changement de filtre.
Les commentaires des utilisateurs sont les suivants : lors du changement rapide d’onglet et de tri, la liste affiche parfois “les éléments de filtre du nouveau tri”, mais les données de la liste sont toujours “les résultats de l’ancien tri”. Cliquer à nouveau sur le même tri fonctionne correctement.
À première vue, il semble que l’interface soit incohérente, mais la capture du paquet montre que l’interface ne renvoie aucun problème et que l’écho sort dans le corps renvoyé est également correct. En d’autres termes, le serveur a raison, et ce qui ne va pas, c’est « l’état affiché » sur le front-end.
Première erreur de jugement : je pensais qu’il s’agissait d’une condition de concurrence critique des demandes
L’intuition vous fera soupçonner : la demande de A est lente, la demande de B est rapide, B revient en premier et rend le résultat correct, puis A revient et écrase l’ancien résultat.
Ce type de condition de concurrence est en effet courant, nous ajoutons donc d’abord le requestId et éliminons le paquet de retour : seule la dernière requête envoyée est acceptée.
Après la connexion, le problème s’est un peu atténué, mais il n’a pas disparu. Expliquez que la « couverture des paquets de retour » n’est pas le seul canal.
L’intérêt de cette étape est la suivante : elle coupe d’abord une partie d’un espace problématique apparemment important. Il est désormais certain qu’au moins certains des états défectueux ne sont pas causés par l’ordre du réseau.
Deuxième erreur d’appréciation : je pensais que c’était une erreur dans la logique du cache.
Regardons ensuite le cache du composant liste.
La stratégie de mise en cache est la suivante :
- Passer
filtersdepuis les accessoires - Le composant liste utilise en interne
useRefpour enregistrerlastGoodData - Déclenchez la requête si
filterschange - Continuer à afficher
lastGoodDatalors de la requête, puis le remplacer lorsque de nouvelles données reviennent
Il n’y a rien de mal à cette logique de « réduction du scintillement », mais elle enterre une prémisse : filters doit être une source de vérité stable et unique. Sinon, il est facile d’apparaître : filters a changé, mais la liste utilise toujours l’ancien lastGoodData, et en apparence, on pensera qu’il ne s’agit que d’une dissimulation lors du chargement.
Je pensais que c’était parce que la référence de l’objet filters était instable, ce qui entraînait une confusion dans le timing de déclenchement de l’effet. Remplacé par une clé de sérialisation explicite : filtersKey = tab + sort + city.
Toujours pas de remède.
La véritable cause : la propriété de l’État est déchiquetée
Après avoir finalement saisi tous les journaux, le problème est devenu clair :
- Le composant Tab ne s’intéresse qu’à
tab, il va : - Lorsque vous cliquez dessus,
setLocalTab(nextTab)sera immédiatement mis en surbrillance - Ensuite,
onChange(nextTab)notifie le composant parent - Accédez à
setFilters({ ... })après avoir reçu le composant parent - Le composant de tri a également le même modèle
- Afin de prendre en charge l’actualisation pouvant être reprise, le composant parent :
- Extrayez d’abord les filtres initiaux de l’analyse d’URL
- Ensuite, écrivez-le pour indiquer
- Les composants de liste reçoivent également :
- Accessoires
filtersdu composant parent - et un
getCachedResult(filtersKey)du module de cache
En d’autres termes, un État dispose d’au moins trois ensembles de sources :
- État local du sous-composant : utilisé pour une interaction de rétroaction immédiate
- L’état du composant parent : en tant que filtres au niveau de la page
- Module de cache : utilisé comme backend d’affichage des données
Il n’y a pas de contrat strict de « commande d’écriture » entre eux.
Le lien où le problème survient est généralement le suivant :
- Tri des points utilisateur
- Le composant Sort met immédiatement à jour son état local et l’interface utilisateur affiche “Nouveau tri sélectionné”.
- Le
filtersdu composant parent n’a pas eu le temps d’être mis à jour (ou il a été mis à jour mais ne sera transmis qu’à la frame suivante) - Le composant liste recalculera
filtersKeyà ce moment - Mais cela ne compte pas les nouveaux filtres du composant parent
- Au lieu de cela, un chemin dérivé mélange la valeur locale de Sort (par exemple via le contexte ou le sélecteur)
filtersKeya changé, donc la liste est allée dans le module de cache et a récupéré un ancien résultat qui “semblait correspondre”- Lorsque la demande revient, en raison de la politique de suppression de requestId, tant que ce n’est pas la dernière fois, elle sera rejetée
- L’interface utilisateur finale a une combinaison étrange : la barre de filtre est la nouvelle valeur et les données de la liste proviennent de l’ancien cache
C’est “plusieurs morceaux de code sont écrits correctement, mais l’ordre est instable”.
La division des composants coupe les droits d’écriture de l’état en morceaux : le composant enfant écrit d’abord une copie pour un retour instantané de l’interaction, le composant parent écrit une autre copie pour la lecture et le cache écrit une autre copie pour le plaisir de l’expérience. Il n’y a rien de mal à cela, mais le système manque d’une propriété d’État unifiée.
Comment arrêter de parler : décidez d’abord de la propriété, puis parlez de réutilisation
La solution à ce type de problème consiste à inscrire les droits d’écriture, les règles de dérivation et les stratégies de dissimulation de l’État dans un contrat exécutoire.
J’ai fini par me contenter de trois règles.
Règle 1 : les filtres de page n’ont qu’une seule source accessible en écriture
Les composants enfants ne conservent plus leur propre état de filtres locaux.
Un retour interactif immédiat est fourni par l’état du composant parent, et le composant enfant est uniquement responsable de l’envoi des événements et non du stockage des valeurs. C’est à dire :
- Sous-ensemble :
onSelect(next) - Composant parent :
setFilters(reduce(prev, action)) - Affichage des sous-composants : lecture seule
value={filters.sort}
Le coût est le suivant : le sous-composant deviendra “stupide” et davantage d’accessoires devront être transmis une fois réutilisés. Mais ce qu’il échange, c’est un certain chemin d’écriture.
Règle 2 : Les valeurs dérivées doivent indiquer explicitement la source
Toutes les clés utilisées pour les requêtes et la mise en cache ne peuvent être générées qu’à partir de filters.
Il est interdit de générer un autre jeu de clés directement à partir du contexte, du sélecteur ou de l’URL.
Cela peut sembler mystique, mais il s’agit d’éviter « les champs portant le même nom provenant de sources différentes ». Une fois que sort est autorisé à provenir à la fois de l’État local et de l’État parent, vous rencontrerez aujourd’hui « un ensemble d’interface utilisateur et un autre ensemble de données ».
Règle 3 : Le cache affiche uniquement les détails et ne participe pas au jugement de statut.
Le module de cache ne fournit qu’une seule fonctionnalité : getLastGoodData(filtersKey).
Il ne peut pas déterminer quelle est la filtersKey actuelle, et encore moins utiliser les données d’une ancienne clé comme “résultat actuel”.
Les étapes spécifiques sont les suivantes :
- Le statut de la demande de liste est clair :
currentFiltersKeyest transmis depuis le composant parent - Autorisé lors de l’affichage :
data = loading ? cache[currentFiltersKey] ?? null : result- mais la mise en cache n’affecte jamais les filtres
Cela rétrograde le cache de « participation à l’état du système » à « affichage pur des résultats ». Cela sacrifiera un peu d’expérience, par exemple, certains commutateurs seront vides pendant un moment, mais cela rendra la certitude.
Contre-exemple : “Promouvoir tous les états en composant parent” échouera également
Après avoir écouté cela, certaines personnes diront : élevez simplement tous les États au niveau supérieur.
La version que j’ai vue échouer est la suivante : la couche supérieure devient la seule source de vérité, mais elle porte également :
- Synchronisation des URL
- Persistance locale
- Demande de limitation
- accès au cache
- État interactif de l’interface utilisateur (survol, focus, panneau ouvert)
En conséquence, le réducteur de niveau supérieur devient un cœur de métier, et toute petite interaction doit passer par un certain nombre de logiques, et le coût de modification monte en flèche. En fin de compte, tout le monde a commencé à le contourner et a secrètement ajouté l’État local à ses sous-composants, et le système est revenu à « de multiples vérités locales ».
La clé est donc de « préciser quels États exigent la propriété exclusive ».
Je le juge généralement en une phrase : tant que cela affecte les requêtes, les clés de cache ou la cohérence entre les composants, il doit appartenir de manière unique. Les transitoires purs de l’interface utilisateur peuvent être localisés.
Limite applicable : toutes les divisions de composants n’entraîneront pas de pièges
Cette critique ne concerne pas le fractionnement des composants en soi.
Le fractionnement était initialement destiné à contrôler la complexité, mais il a un coût implicite : la « frontière de l’écriture de l’état » doit en outre être conçue.
Lorsque la page remplit l’une des conditions suivantes, ce piège apparaîtra facilement :
- A une URL/récupération persistante
- Il y a des demandes ou des annulations simultanées
- Il y a un affichage en cache
- Avoir des filtres partagés entre les composants
D’un autre côté, si la page est très simple, que l’état n’affecte pas la demande et qu’il n’y a pas de mise en cache ni de récupération, l’état local ne deviendra pas un désastre.
Résumé
La décomposition des composants en petits morceaux n’apportera pas automatiquement des avantages en matière de réutilisation ; cela créera d’abord des problèmes de propriété étatique.
Si vous ne répondez pas en premier « Qui peut écrire, qui connaît les détails et qui est responsable du timing », le système répondra tout seul : celui qui rend le premier aura le dernier mot, et celui qui reviendra en retard le couvrira.
Lorsque des états d’erreur “occasionnels” se produisent sur la ligne, vous constaterez que ce qui coûte vraiment cher est de récupérer un état à partir de plusieurs vérités partielles dans un certain lien d’écriture. \
What to read next
Want more posts about Uncategorized?
Posts in the same category are usually the best next step for reading more on this topic.
View same categoryWant to explore another direction?
If you are not sure what to read next, return to the homepage and start from categories, topics, or latest updates.
Back home