Un moyen plus simple de faire du code impératif dans les applications React
Quelle que soit notre capacité à écrire des applications React, nous ne pouvons pas entièrement éviter de coder par rapport à des interfaces impératives. En effet, l’interface utilisateur est fondamentalement impérative, tout comme les API du navigateur.
Le contrôle des éléments multimédias est un cas courant, mais nous pouvons également avoir affaire à des bibliothèques tierces qui respectent les conventions des navigateurs. Le modèle déclaratif de React ne s’intègre pas bien aux API impératives, et nous devons en quelque sorte combler le fossé entre le « quoi » et le « comment ». Dans cet article, nous discutons d’un tel modèle.
Les gens qui me suivent savent que je suis un fervent partisan de Vanilla JavaScript. Une leçon importante que j’ai tirée des aventures de la vanille est que chaque solution a un endroit naturel où elle doit être mise en œuvre, et s’écarter de cet endroit crée un passe-partout et de la complexité. Si vous avez déjà ressenti que l’utilisation de useEffect
et useRef
, et le suivi manuel des dépendances de hook semble un peu décalé par rapport aux mises à jour de l’interface utilisateur basées sur l’état, c’est parce que c’est le cas. Il ne correspond pas à ce modèle (jeu de mots), et il n’appartient pas à l’interface utilisateur réactive.
Le modèle de pont déclaratif-impératif vise à résoudre ce problème et à placer le pont entre les univers déclaratif et impératif dans un contexte plus Naturel place. Par lieu naturel, j’entends un lieu où un bien défini existe déjà un mécanisme qui fait exactement ce dont nous avons besoin. Dans le cas du pont déclaratif-impératif, l’endroit où cela se produit naturellement est le mécanisme d’observation des attributs dans le éléments personnalisés.
Il s’agit d’un modèle relativement nouveau que nous n’aurions pu utiliser de manière réaliste sur nos navigateurs cibles que depuis environ 2020, lorsque la spécification des éléments personnalisés est devenu pris en charge dans tous les navigateurs après que Microsoft a finalement abandonné son ancien navigateur Edge et est passé à un navigateur basé sur Chromium. Safari ne prend toujours pas en charge toutes les spécifications (merci, Apple !), mais cela n’a aucune incidence sur ce que nous avons l’intention de faire ici.
(Insérez le bon vieil exemple en utilisant les références et les effets ici pour être complet.)
Je vais supposer que vous connaissez la manière habituelle de le faire en utilisant des références et des effets. Je ne vais pas vous ennuyer avec des exemples de ce modèle. Plongeons directement dans le DIB.
La partie React reste à peu près la même que n’importe quel composant réactif piloté par l’état.
Par exemple:
let
STOPPED = 0,
PLAYING = 1let Player = () => {
let
[playbackState, setPlaybackSate] = useState(STOPPED),
onPlay = () => setPlaybackState(PLAYING),
onStop = () => setPlaybackState(STOPPED)
return (
<article>
<react-audio playback-state={playbackState}>
<audio src="foo.mp3"></audio>
</react-audio>
<button onClick={onPlay}
aria-pressed={playbackState === PLAYING}>
Play
</button>
<button onClick={onStop}>
Stop
</button>
</article>
)
}
J’espère que je n’ai pas besoin d’expliquer ce que fait le code ci-dessus, car c’est un peu le but du modèle DIB – la simplicité.
Je soulignerai cependant un petit détail. La <audio>
l’élément est un élément enfant du <react-audio>
élément personnalisé. Ceci est important dans la mesure où React a un contrôle total sur le sous-arbre de l’élément personnalisé. Dans l’exemple de code, React est celui qui spécifie la source et, en fait, crée le <audio>
élément. Si nous voulions être informés de l’heure actuelle pendant la lecture, nous pourrions joindre un onTimeUpdate
gestionnaire d’événements au <audio>
élément directement plutôt que l’élément personnalisé.
Jetons un coup d’œil à l’élément personnalisé où se produit le véritable pont déclaratif-impératif. Nous appelons cet élément l’élément ‘pont’.
customElements.define('react-audio', class extends HTMLElement {
static get observedAttributes() { return ['playback-state'] }attributeChangedCallback() {
if (this.getAttribute('playback-state') == STOPPED) {
this.firstElementChild.pause()
this.firstElementChild.currentTime = 0
} else {
this.firstElementChild.play()
}
}
})
L’élément personnalisé déclare le playback-state
attribut tel qu’observé. Cela entraînera la attributeChangedCallback()
méthode d’instance à invoquer chaque fois que l’attribut change.
Dans le attributeChangedCallbak()
nous traduisons le concept déclaratif d’état de lecture en l’action impérative exécutée sur le <audio>
élément, qui est le premier (et unique) enfant de l’élément personnalisé.
Gardez à l’esprit que cela peut devenir plus complexe à mesure que nos besoins augmentent. Nous pouvons avoir plusieurs attributs observés, nous pouvons émettre des événements, etc. En parlant d’événements, notez qu’en raison de son événement synthétique mise en œuvre, React est limité à une poignée d’événements standard. Nous ne pouvons pas profiter des événements personnalisés.
La <react-audio>
L’élément n’a pas besoin de créer de nœuds enfants, car ce n’est pas son objectif. Puisque nous travaillons avec des éléments personnalisés, nous avons toujours le option de rendre le sous-arbre entier, même en utilisant DOM fantôme aux fins. Ce faisant, nous abandonnons un certain contrôle sur les nœuds enfants du côté React, mais ouvrons les portes à d’autres choses (par exemple, la réutilisation du code dans l’application en utilisant différents frameworks, voire aucun framework). Le fait est que le rendu du sous-arbre dans React ou dans l’élément personnalisé n’est pas pertinent pour le modèle DIB, et le modèle ne vous oblige pas spécifiquement à le faire d’une manière ou d’une autre.
Bien sûr que non. Exactement!
L’intérêt de ce modèle est de déplacer le code impératif – et donc non spécifique à React – hors des crochets d’effet et hors des composants, où il ne rentre pas tout à fait de toute façon. C’est le code que vous auriez besoin d’écrire quelque part quoi qu’il en soit, mais l’envelopper dans un crochet ne le rend pas plus « réactif » et « piloté par l’État » qu’un rouleau de papier toilette. Je le vois comme un code « étranger » intégré dans un composant React, un peu comme un animal sauvage qui errait dans le jardin de quelqu’un. Il est sauvé et déplacé dans son habitat naturel.
Pour récapituler, le modèle DIB fonctionne comme suit :
- React fait ce que React fait le mieux – changements d’interface utilisateur basés sur l’état – nous évitons simplement de faire tout code impératif (effets) dans les composants
- L’écart entre les API déclaratives et impératives est comblé par la fonctionnalité d’attribut observé des éléments personnalisés
- Tout le code impératif vit dans l’élément personnalisé
- L’élément personnalisé peut ou non utiliser un sous-arbre créé par React, il n’a aucun rapport avec le modèle
Vous trouverez le code source entièrement fonctionnel d’une implémentation DIB plus complète dans mon référentiel GitHub.