Construire des solutions de gestion d'état pour les éditeurs des outils créatifs

Introduction
Dans cet article, nous partagerons certaines de nos idées (légèrement opiniâtres) sur la conception et la construction de solutions de gestion d'état évolutives dans le contexte d'éditeurs d'outils créatifs avec React, Immer et Recoil comme fondements de notre approche.
Pour mieux illustrer notre approche, nous allons inclure quelques exemples de code et un petit prototype.
Nous avons utilisé ces connaissances dans quelques projets différents, ce qui nous a permis d'échapper facilement à certains des pièges les plus courants.
État de l'application vs état de conception
L'une des principales distinctions que nous pouvons faire dans ces types d'applications est l'état de l'application par rapport à l'état de conception.
État de conception
Décrit le contenu créé par l'utilisateur, par exemple : texte (police, taille, couleur, contenu), images, éléments groupés, éléments connectés, etc.
L'état de la conception change chaque fois que la conception change conceptuellement, mais pas lorsque l'utilisateur effectue des actions qui n'ont pas d'impact sur la conception.
Quelques exemples d'actions impactant l'état de conception :
- Déplacer un élément
- Modifier les propriétés d'un texte (couleur, taille, police)
- Regrouper les éléments
Définit l'état actuel de l'application, à l'exclusion de la conception. L'état de l'application change lorsque l'utilisateur interagit avec l'application et ses menus.
Quelques exemples:
- Sélectionnez un élément
- Ouvrir une boîte de dialogue pour modifier les propriétés d'un élément
- Afficher le menu contextuel
- Confirmer une action
Chaque type d'état a ses propres particularités, qui ne s'appliquent pas à l'autre, et rendraient notre mise en œuvre plus complexe si elles étaient maintenues ensemble.
Par exemple:
- Lors de l'enregistrement d'un dessin, seule cette partie de l'état est enregistrée.
- Annuler/rétablir ne s'applique qu'à l'état de conception. L'application doit pouvoir fonctionner avec de petites incohérences entre la conception et l'état de l'application, par exemple, lorsque l'élément sélectionné n'est plus dans la conception en raison d'une annulation.
- Lors du chargement d'un design, l'état du design est simplement défini avec le design, tandis que l'état de l'application est principalement initialisé avec une valeur par défaut.
État de l'application
Chaque fois que nous le pouvons, nous devons privilégier l'état local à l'état global, l'état de l'application ne fait pas exception.
L'analyse du code et de son flux devient plus facile lorsque nous utilisons l'état local, les composants doivent explicitement transmettre des données entre eux et leurs interactions sont explicites, il n'y a pas d'effets d'utilisation "cachés lointains" que nous devons rechercher manuellement.
État de conception
Lors de la définition de l'état de conception, nous devons avoir les considérations suivantes :
- Être capable d'obtenir et de définir l'état complet afin que nous puissions facilement implémenter :
– Charger et enregistrer une conception
– Annuler/rétablir - Évitez de rendre tous les éléments lorsque seuls certains d'entre eux changent
L'un des moyens les plus simples d'implémenter undo/redo dans une application React consiste à utiliser Immer pour générer des correctifs de modification.
Immer est une bibliothèque JS qui nous permet d'écrire des modifications de données dans un contexte immuable (par exemple l'état React) de manière "mutable".
Une autre fonctionnalité d'Immer est la génération de correctifs d'exécution et d'annulation d'une modification, cela nous permet d'enregistrer comment ré-exécuter la modification que nous avons apportée et comment revenir à l'état des aperçus.
Après chaque changement d'état, nous devons enregistrer les correctifs à faire et à annuler, et les utiliser lorsque l'utilisateur déclenche l'annulation/le rétablissement.
Pourquoi avons-nous besoin d'obtenir et de définir tout l'état en même temps ?
L'alternative est de ne pas l'avoir ensemble, cela ne semble pas être un gros problème pour le chargement et la sauvegarde, nous devons juste obtenir/définir toutes les différentes parties d'état.
Mais cela devient un problème lorsque nous devons implémenter undo/redo. Pour chaque partie d'état, nous devons enregistrer les correctifs produits, avec des métadonnées indiquant à quelle partie d'état il s'adresse, et les lire lorsqu'une annulation est déclenchée afin de pouvoir modifier la bonne partie d'état.
De plus, étant donné qu'une action utilisateur peut modifier plusieurs parties d'état en une seule opération, nous devons garder une trace des correctifs appartenant à la même opération afin de pouvoir les annuler et les rétablir en même temps.
L'utilisation d'un seul état résoudrait toutes ces choses
- Aucune action ne modifie plusieurs états
- Tous les correctifs s'appliquent à l'état, et non à plusieurs états distribués.
Le moyen le plus simple de satisfaire ce choix de conception consiste à enregistrer l'ensemble de l'état de conception au même endroit. Cela nous amènerait à penser que useState<DesignState>(defaultState)
, comme vous le devinez probablement, cela nous fait échouer notre considération "rendre la majeure partie de l'application":
Ne rend pas la majeure partie de l'application lorsque la conception change
Pour résoudre ce problème, nous utilisons généralement Recoil, une bibliothèque de gestion d'état pour React.
Recoil a deux concepts principaux : les atomes et les sélecteurs.
Atomes : unité d'état, qui peut être utilisée de la même manière que useState
, mais globalement, par exemple
En tant useState
qu'implémentation, dans le code ci-dessus, tous les DesignElements seront rendus chaque fois qu'un élément (ou une partie de l'état) change. Pour résoudre ce problème, nous pouvons utiliser des sélecteurs.
Sélecteurs : fonctions qui créent une projection à partir d'un atome ou d'un autre sélecteur. Lorsqu'un composant React utilise un sélecteur, il ne sera restitué que (avec quelques mises en garde) lorsque le résultat du sélecteur change.
Recoil nous permet également de recevoir des arguments dans la fonction qui définit le sélecteur, ces types de sélecteurs sont appelés selectorFamily. Nous pouvons l'utiliser pour créer un selectorFamily qui reçoit un elementId et nous donne l'élément.
Lorsqu'un élément est modifié, le code ci-dessus déclenche uniquement une mise à jour du DesignElement correspondant et ne restitue pas tous les éléments ou le composant Design.
Un observateur attentif peut voir que le composant Design sera rendu chaque fois qu'un composant est ajouté ou supprimé, déclenchant le nouveau rendu de tous les DesignElements. Si cela pose des problèmes de performances pour un cas d'utilisation particulier, nous pouvons envelopper notre composant DesignElement dans un fichier React.memo
.
Définir l'état
Parce que nous voulons que nos ensembles soient appliqués au DesignState de niveau supérieur (pour simplifier l'annulation/rétablissement), nous pouvons créer des rappels de recul pour encapsuler notre logique de modification et annuler/rétablir la création de correctifs.
Un exemple minimal peut être trouvé dans cette Codesanbox.
Fermeture
Dans cet article de blog, nous avons partagé quels sont les principaux moteurs lors de la conception de la gestion des états dans le contexte des éditeurs d'outils créatifs, et notre implémentation de référence qui les satisfait.
Si vous l'avez lu jusqu'au bout, j'aimerais connaître votre opinion. Vous pouvez me joindre ici .
Chez Zeppelin Labs, nous aidons les fondateurs et les entreprises en croissance à expérimenter et à créer des produits numériques différenciés qui stimulent la croissance. Vous pouvez nous trouver ici . Ou ici . Ou ici .
Si vous souhaitez rejoindre notre équipe, écrivez-nous à [email protected]
Souhaitez-vous être partenaire? écrivez-nous à [email protected]
Abonnez-vous à notre newsletter ici .