Fermer

mai 10, 2021

Commencez avec React en créant un jeu WhacAMole


À propos de l'auteur

Jhey fait des choses géniales pour des gens formidables! C'est un développeur Web avec près de 10 ans d'expérience. Travailler avec et pour des noms tels que Eurostar, Uber,…
En savoir plus sur
Jhey

Vous voulez commencer avec React mais vous avez du mal à trouver un bon point de départ? Cet article devrait vous couvrir. Nous nous concentrerons sur certains des principaux concepts de React, puis nous créerons un jeu à partir de zéro! Nous supposons que vous avez une connaissance pratique de JavaScript. Ah, et si vous êtes ici pour le jeu, vous pouvez commencer tout de suite .

Je travaille avec React depuis la sortie de ~ v0.12. (2014! Wow, où est passé le temps?) Cela a beaucoup changé. Je me souviens de certains moments «Aha» en cours de route. Une chose qui est restée est l'état d'esprit pour l'utiliser. Nous pensons aux choses d'une manière différente plutôt que de travailler avec le DOM direct.

Pour moi, mon style d'apprentissage est de faire fonctionner quelque chose aussi vite que possible. Ensuite, j'explore des zones plus profondes de la documentation et tout ce qui est inclus chaque fois que nécessaire. Apprenez en faisant, en vous amusant et en poussant les choses!

Objectif

Le but ici est de vous montrer suffisamment de React pour couvrir certains de ces moments «Aha». Vous laissant assez curieux pour creuser vous-même les choses et créer vos propres applications.
Je recommande de consulter la documentation pour tout ce que vous voulez approfondir. Je ne les dupliquerai pas.

Veuillez noter que vous pouvez trouver tous les exemples dans CodePen mais vous pouvez également passer à mon dépôt Github pour un fonctionnement complet jeu.

Première application

Vous pouvez démarrer une application React de différentes manières. Voici un exemple:

 import React depuis 'https://cdn.skypack.dev/react'
importer {render} depuis 'https://cdn.skypack.dev/react-dom'

const App = () => 

{`Heure: $ {Date.now ()}`}

render (document.getElementById ('app')

Voir le stylo [Your First React App] (https://codepen.io/smashingmag/pen/xxqGYWg) par @ jh3y .

Voir le stylo Votre première application React par @ jh3y .

C'est à peu près tout ce dont vous avez besoin pour créer votre première application React (en plus du HTML) pour commencer. Mais, nous pourrions réduire cela, comme ceci:

 render (

{`Time: $ {Date.now ()}`}

document.getElementById ('app'))

Dans la première version, App est un composant, mais cet exemple indique à React DOM de rendre un élément au lieu d'un composant . Les éléments sont les éléments HTML que nous voyons dans les deux exemples. Qu'est-ce qui fait un composant, est une fonction renvoyant ces éléments

Avant de commencer avec les composants, quel est le problème avec ce «HTML en JS»?

JSX

Ce «HTML en JS» est JSX. Vous pouvez tout lire sur JSX dans la documentation React . L'essentiel? Une extension de syntaxe à JavaScript qui nous permet d'écrire du HTML en JavaScript. C'est comme un langage de création de modèles avec un accès complet aux pouvoirs JavaScript. C'est en fait une abstraction sur une API sous-jacente. Pourquoi l'utilisons-nous? Pour la plupart, il est plus facile à suivre et à comprendre que l'équivalent.

 React.createElement ('h1', null, `Time: $ {Date.now ()}`) 

La ​​chose à prendre en compte avec JSX est que c'est ainsi que vous mettez les choses dans le DOM 99% du temps avec React. Et c'est aussi la façon dont nous lions la gestion des événements la plupart du temps. Cet autre 1% est un peu hors de portée de cet article. Mais, parfois, nous voulons rendre des éléments en dehors des domaines de notre application React. Nous pouvons le faire en utilisant le portail de React DOM. Nous pouvons également avoir un accès direct au DOM dans le cycle de vie du composant (à venir).

Les attributs dans JSX sont camelCase. Par exemple, onclick devient onClick . Il existe quelques cas particuliers tels que class qui devient className . De plus, des attributs tels que style acceptent désormais un Object au lieu d'une chaîne .

 
Cool

Note : Vous pouvez vérifier toutes les différences d'attributs ici .

Rendu

Comment pouvons-nous intégrer notre JSX dans le DOM? Nous devons l'injecter. Dans la plupart des cas, nos applications ont un point d'entrée unique. Et si nous utilisons React, nous utilisons React DOM pour insérer un élément / composant à ce stade. Vous pouvez cependant utiliser JSX sans React. Comme nous l'avons mentionné, il s'agit d'une extension de syntaxe . Vous pouvez changer la façon dont JSX est interprété par Babel et le faire pomper quelque chose de différent .

Tout ce qu'il contient est géré par React. Cela peut apporter certains avantages en termes de performances lorsque nous modifions beaucoup le DOM. C'est parce que React utilise un DOM virtuel. Faire des mises à jour DOM n'est en aucun cas lent. Mais c'est son impact sur le navigateur qui peut avoir un impact sur les performances. Chaque fois que nous mettons à jour le DOM, les navigateurs doivent calculer les changements de rendu qui doivent avoir lieu. Cela peut coûter cher. En utilisant le DOM virtuel, ces mises à jour DOM sont conservées en mémoire et synchronisées avec le DOM du navigateur par lots si nécessaire.

Rien ne nous empêche d'avoir de nombreuses applications sur une page ou d'avoir seulement une partie d'une page gérée par React. [19659030] Voir le stylo [Two Apps] (https://codepen.io/smashingmag/pen/QWpbmWw) par @ jh3y .

Voir le stylo Deux applications par ] @ jh3y .

Prenons cet exemple. La même application est rendue deux fois entre du HTML ordinaire. Notre application React affiche l'heure actuelle en utilisant Date.now .

 const App = () => 

{`Time: $ {Date.now ()}`}

Pour cet exemple, nous effectuons le rendu de l'application deux fois entre du code HTML standard. Nous devrions voir le titre «Many React Apps» suivi d'un texte, puis le premier rendu de notre application apparaît suivi d'un texte, puis le deuxième rendu de notre application.

Pour une analyse plus approfondie du rendu, consultez la documentation .

Composants et accessoires

C'est l'une des parties les plus importantes de React to grok. Les composants sont des blocs d'interface utilisateur réutilisables. Mais en dessous, tout est fonctionnel. Les composants sont des fonctions dont nous appelons les arguments props . Et nous pouvons utiliser ces «accessoires» pour déterminer ce qu'un composant doit rendre. Les accessoires sont en «lecture seule» et vous pouvez tout passer dans un accessoire. Même d'autres composants. Tout ce qui se trouve dans les balises d'un composant auquel nous accédons via un accessoire spécial, children .

Les composants sont des fonctions qui renvoient des éléments. Si nous ne voulons rien afficher, renvoyez null .

Nous pouvons écrire des composants de différentes manières. Mais c’est le même résultat.

Utilisez une fonction:

 function App () {
  return 

{`Heure: $ {Date.now ()}`}

}

Utilisez une classe:

 class App extend React.Component {
  render () {
    return 

{`Heure: $ {Date.now ()}`}

} }

Avant la sortie des hooks (à venir), nous utilisions beaucoup des composants basés sur les classes. Nous en avions besoin pour l'état et l'accès à l'API du composant. Mais, avec les hooks, l'utilisation de composants basés sur les classes a un peu ralenti. En général, nous optons toujours pour composants basés sur la fonction maintenant. Cela présente divers avantages. D'une part, il faut moins de code pour obtenir le même résultat. Les hooks facilitent également le partage et la réutilisation de la logique entre les composants. De plus, les cours peuvent prêter à confusion. Ils ont besoin que le développeur comprenne les liaisons et le contexte.

Nous utiliserons des fonctions basées sur des fonctions et vous remarquerez que nous avons utilisé un style différent pour notre composant App .

 const App = () => 

{`Heure: $ {Date.now ()}`}

C'est valable. L'essentiel est que notre composant renvoie ce que nous voulons rendre. Dans ce cas, un élément unique qui est un h1 affichant l'heure actuelle. Si nous n’avons pas besoin d’écrire return etc., alors ne le faites pas. Mais, tout est de préférence. Et différents projets peuvent adopter des styles différents.

Et si nous mettions à jour notre exemple multi-applis pour accepter les accessoires et que nous extrayions le h1 en tant que composant?

 const Message = ({message}) => 

{message}

const App = ({message}) => render (document.getElementById ('app'))

Voir le stylo [Our First Component Extraction] (https://codepen.io/smashingmag/pen/rNyVJKe) par @ jh3y .

Voir le stylo Our First Component Extraction par @ jh3y .

Cela fonctionne et maintenant nous pouvons changer le message prop sur App et nous obtiendrions différents messages. Nous aurions pu créer le composant Time . Mais, la création d'un composant Message implique de nombreuses possibilités de réutiliser notre composant. C'est la chose la plus importante à propos de React. Il s’agit de prendre des décisions concernant l’architecture / la conception.

Et si nous oublions de transmettre l’élément à notre composant? Nous pourrions fournir une valeur par défaut. Voici quelques façons de le faire.

 const Message = ({message = "Vous m'avez oublié!"}) => 

{message}

Ou en spécifiant defaultProps sur notre composant. Nous pouvons également fournir propTypes ce que je vous recommande d’examiner. Il fournit un moyen de taper des accessoires de vérification sur nos composants.

 Message.defaultProps = {
  message: "Vous m'avez oublié!"
}

Nous pouvons accéder aux accessoires de différentes manières. Nous avons utilisé les commodités ES6 pour déstructurer les accessoires. Mais notre composant Message pourrait également ressembler à ceci et fonctionner de la même manière.

 const Message = (props) => 

{props.message}

Les accessoires sont un objet transmis au composant. Nous pouvons les lire comme bon nous semble.

Notre composant App pourrait même être le suivant:

 const App = (props) => 

Cela donnerait le même résultat. Nous nous référons à cela sous le nom de «Prop spreading». Il vaut mieux cependant être explicite avec ce que nous traversons.

Nous pourrions aussi transmettre le message en tant qu'enfant.

 const Message = ({children}) => 

{children}

const App = ({message}) => {message}

Ensuite, nous renvoyons au message via l'accessoire spécial enfants .

Que diriez-vous d'aller plus loin et de faire quelque chose comme demander à notre App de passer un message ] à un composant qui est également un accessoire.

 const Time = ({children}) => 

{`Time: $ {children}`}

const App = ({message, messageRenderer: Renderer}) => {message} render (document.getElementById ('app'))

Voir le stylo [Passing Components as Props] (https://codepen.io/smashingmag/pen/xxqGYJq) par @ jh3y .

Voir le stylo Passer des composants comme accessoires par @ jh3y .

Dans cet exemple, nous créons deux applications et une rend l'heure et une autre un message. Remarquez comment nous renommons l'accessoire messageRenderer en Renderer dans la déstructure? React ne verra rien commençant par une lettre minuscule en tant que composant. En effet, tout ce qui commence en minuscules est considéré comme un élément. Il le rendrait comme . Nous utiliserons rarement ce modèle, mais c'est un moyen de montrer comment tout ce qui peut être un accessoire et que vous pouvez en faire ce que vous voulez.

Une chose à préciser est que tout ce qui est passé comme accessoire doit être traité par le composant. Par exemple, si vous souhaitez transmettre des styles à un composant, vous devez les lire et les appliquer à tout ce qui est rendu.

N'ayez pas peur d'expérimenter différentes choses. Essayez différents modèles et pratiquez. L'habileté de déterminer ce qui devrait être un composant vient de la pratique. Dans certains cas, c'est évident, et dans d'autres, vous pourriez vous en rendre compte plus tard et refactoriser.

Un exemple courant serait la mise en page d'une application. Pensez à un niveau élevé à quoi cela pourrait ressembler. Une mise en page avec des enfants qui comprend un en-tête, un pied de page, un contenu principal. À quoi cela pourrait-il ressembler? Cela pourrait ressembler à ceci.

 const Layout = ({children}) => (
  
{enfants}
)

Tout est question de blocs de construction. Pensez-y comme LEGO pour les applications.

En fait, une chose que je préconiserais est de se familiariser avec Storybook dès que possible (je créerai du contenu là-dessus si les gens veulent le voir ). Le développement axé sur les composants n'est pas unique à React, nous le voyons également dans d'autres frameworks. Changer votre mentalité pour penser de cette façon vous aidera beaucoup.

Apporter des changements

Jusqu'à présent, nous n'avons traité que du rendu statique. Rien ne change. La chose la plus importante à prendre en compte pour apprendre React est le fonctionnement de React. Nous devons comprendre que les composants peuvent avoir un état. Et nous devons comprendre et respecter que l'État dirige tout. Nos éléments réagissent aux changements d'état. Et React ne restituera que si nécessaire.

Le flux de données est également unidirectionnel. Comme une cascade, les changements d'état descendent dans la hiérarchie de l'interface utilisateur. Les composants ne se soucient pas de l'origine des données. Par exemple, un composant peut vouloir passer l'état à un enfant via des accessoires. Et ce changement peut déclencher une mise à jour du composant enfant. Ou bien, les composants peuvent choisir de gérer leur propre état interne qui n’est pas partagé.

Ce sont toutes des décisions de conception qui deviennent plus faciles à mesure que vous travaillez avec React. La principale chose à retenir est à quel point ce flux est unidirectionnel. Pour déclencher des changements plus haut, cela doit se produire via des événements ou d'autres moyens passés par les accessoires.

Créons un exemple.

 import React, {useEffect, useRef, useState} de 'https: // cdn. skypack.dev/react '
importer {render} depuis 'https://cdn.skypack.dev/react-dom'

const Heure = () => {
  const [time, setTime] = useState (Date.now ())
  const timer = useRef (null)
  useEffect (() => {
    timer.current = setInterval (() => setTime (Date.now ()), 1000)
    return () => clearInterval (timer.current)
  }, [])
  return 

{`Time: $ {time}`}

} const App = () =>

Voir le stylo [An Updating Timer] (https://codepen.io/smashingmag/pen/MWpwQqa) par @ jh3y .

Voir le stylo An Updating Timer by @ jh3y .

Il y a pas mal à digérer là-bas. Mais, ici, nous introduisons l'utilisation de «Hooks». Nous utilisons «useEffect», «useRef» et «useState». Ce sont des fonctions utilitaires qui nous donnent accès à l'API du composant.

Si vous cochez l'exemple, l'heure est mise à jour toutes les secondes ou 1000ms . Et cela est motivé par le fait que nous mettons à jour l’heure qui est un morceau d’État. Nous faisons cela dans un setInterval . Notez que nous ne modifions pas directement l'heure . Les variables d'état sont traitées comme immuables. Nous le faisons par la méthode setTime que nous recevons en invoquant useState . Chaque fois que l'état est mis à jour, notre composant effectue un nouveau rendu si cet état fait partie du rendu. useState renvoie toujours une variable d'état et un moyen de mettre à jour cet état. L'argument passé est la valeur initiale de cet élément d'état.

Nous utilisons useEffect pour nous connecter au cycle de vie du composant pour des événements tels que des changements d'état. Les composants se montent lorsqu'ils sont insérés dans le DOM. Et ils sont démontés lorsqu'ils sont supprimés du DOM. Pour nous connecter à ces étapes du cycle de vie, nous utilisons des effets. Et nous pouvons renvoyer une fonction dans cet effet qui se déclenchera lorsque le composant sera démonté. Le deuxième paramètre de useEffect détermine quand l'effet doit s'exécuter. Nous l'appelons le tableau de dépendances. Tous les éléments répertoriés qui changent déclenchent l'exécution de l'effet. Aucun deuxième paramètre ne signifie que l'effet s'exécutera sur chaque rendu. Et un tableau vide signifie que l'effet ne s'exécutera que sur le premier rendu. Ce tableau contiendra généralement des variables d'état ou des accessoires.

Nous utilisons un effet pour à la fois configurer et démonter notre minuterie lorsque le composant monte et démonte.

Nous utilisons une ref pour référencer cela minuteur. Une ref fournit un moyen de garder une référence à des choses qui ne déclenchent pas le rendu. Nous n'avons pas besoin d'utiliser l'état pour le minuteur. Cela n’affecte pas le rendu. Mais, nous devons garder une référence à celui-ci afin que nous puissions l'effacer lors du démontage.

Voulez-vous creuser un peu dans les crochets avant de passer à autre chose? J'ai déjà écrit un article à leur sujet – « React Hooks in 5 Minutes ». Et il y a aussi des informations intéressantes dans la documentation React .

Notre composant Time a son propre état interne qui déclenche les rendus. Mais que se passerait-il si nous voulions changer la longueur de l'intervalle? Nous pourrions gérer cela par le haut dans notre composant App .

 const App = () => {
  const [interval, updateInterval] = useState (1000)
  revenir (
    
      
  )
}

Notre nouvelle valeur d'intervalle est stockée dans l'état de App . Et il dicte la vitesse à laquelle le composant Time se met à jour.

Le composant Fragment est un composant spécial auquel nous avons accès via React . Dans React un composant doit renvoyer un seul enfant ou null . Nous ne pouvons pas renvoyer les éléments adjacents. Mais parfois, nous ne voulons pas envelopper notre contenu dans une div . Les fragments nous permettent d'éviter les éléments wrapper tout en gardant React heureux.

Vous remarquerez également que notre premier événement se lie à cet endroit. Nous utilisons onChange comme attribut de l'entrée pour mettre à jour l'intervalle .

L'intervalle mis à jour est ensuite passé à ] Le temps et le changement de intervalle déclenche l'exécution de notre effet. En effet, le deuxième paramètre de notre crochet useEffect contient désormais intervalle .

 const Time = ({interval}) => {
  const [time, setTime] = useState (Date.now ())
  const timer = useRef (nul)
  useEffect (() => {
    timer.current = setInterval (() => setTime (Date.now ()), intervalle)
    return () => clearInterval (timer.current)
  }, [interval])
  return 

{`Time: $ {time}`}

}

Jouez avec la démo et voyez les changements!

Voir le stylo [Managed Interval] (https://codepen.io/smashingmag/pen/KKWpQGK) par @ jh3y .

] Voir le Pen Intervalle géré par @ jh3y .

Je vous recommande de visiter la documentation React si vous voulez approfondir certains de ces concepts. Mais nous avons vu suffisamment de React pour commencer à créer quelque chose d'amusant! Allons-y!

Whac-A-Mole React Game

Êtes-vous prêt? Nous allons créer notre propre "Whac-A-Mole" avec React! Ce jeu bien connu est basique en théorie mais présente des défis intéressants à construire. La partie importante ici est la façon dont nous utilisons React. Je passerai sous silence l’application des styles et je les rendrai jolis – c’est votre travail! Cependant, je suis heureux de répondre à toutes vos questions à ce sujet.

De plus, ce jeu ne sera pas «poli». Mais ça marche. Vous pouvez vous l'approprier! Ajoutez vos propres fonctionnalités, etc.

Conception

Commençons par réfléchir à ce que nous devons faire, c'est-à-dire aux composants dont nous pourrions avoir besoin, etc.:

  • Démarrer / Arrêter le jeu
  • Minuterie
  • Garder le score
  • Disposition
  • Composant Mole
 Whac-A-Mole Sketch
Whac-A-Mole Sketch ( Grand aperçu )

Point de départ

] Nous avons appris à créer un composant et nous pouvons évaluer approximativement ce dont nous avons besoin.

 import React, {Fragment} de 'https://cdn.skypack.dev/react'
importer {render} depuis 'https://cdn.skypack.dev/react-dom'

const Moles = ({enfants}) => 
{enfants}
const Mole = () => const Timer = () =>
Heure: 00:00
Const Score = () =>
Score: 0
const Jeu = () => (

Whac-A-Mole

) render (document.getElementById ('app'))

Démarrage / Arrêt

Avant de faire quoi que ce soit, nous devons pouvoir démarrer et arrêter le jeu. Le démarrage du jeu déclenchera des éléments tels que la minuterie et les taupes pour prendre vie. C'est ici que nous pouvons introduire rendu conditionnel .

 const Game = () => {
  const [playing, setPlaying] = useState (faux)
  revenir (
    
      {! jouer && 

Whac-A-Mole

} {en jouant && ( )}
) }

Nous avons une variable d'état de jouant et nous l'utilisons pour rendre les éléments dont nous avons besoin. Dans JSX, nous pouvons utiliser une condition avec && pour rendre quelque chose si la condition est vraie . Ici, nous disons de rendre le tableau et son contenu si nous jouons. Cela affecte également le texte du bouton où nous pouvons utiliser un ternaire .

Voir le stylo [1. Toggle Play State] (https://codepen.io/smashingmag/pen/gOmpvBB) par @ jh3y .

Voir le stylo 1. Activer / désactiver l'état de lecture par @ jh3y .

Minuterie

Lançons la minuterie . Par défaut, nous allons définir une limite de temps de 30000ms et nous pouvons déclarer cela comme une constante en dehors de nos composants React.

 const TIME_LIMIT = 30000

Déclarer des constantes en un seul endroit est une bonne habitude à prendre. Tout ce qui peut être utilisé pour configurer votre application peut être colocalisé en un seul endroit.

Notre composant Timer ne se soucie que de trois choses:

  1. Le compte à rebours;
  2. À quoi intervalle qu'il va mettre à jour;
  3. Ce qu'il fait quand il se termine.

Une première tentative pourrait ressembler à ceci:

 const Timer = ({time, interval = 1000, onEnd}) => {
  const [internalTime, setInternalTime] = useState (heure)
  const timerRef = useRef (heure)
  useEffect (() => {
    if (internalTime === 0 && onEnd) onEnd ()
  }, [internalTime, onEnd])
  useEffect (() => {
    timerRef.current = setInterval (
      () => setInternalTime (internalTime - intervalle),
      intervalle
    )
    return () => {
      clearInterval (timerRef.current)
    }
  }, [])
  return  {`Time: $ {internalTime}`} 
}

Mais, il ne se met à jour qu'une seule fois?

Voir le stylo [2. Attempted Timer] (https://codepen.io/smashingmag/pen/yLMNvQN) par @ jh3y .

Voir le stylo 2. Tentative de temporisation par @ jh3y .

Nous utilisons la même technique d'intervalle que nous utilisions auparavant. Mais le problème est que nous utilisons l'état dans notre rappel d'intervalle . Et c'est notre premier "gotcha". Comme nous avons un tableau de dépendances vide pour notre effet, il ne s'exécute qu'une seule fois. La fermeture de setInterval utilise la valeur de internalTime du premier rendu. C'est un problème intéressant qui nous fait réfléchir à la façon dont nous abordons les choses.

Note : Je recommande vivement de lire cet article de Dan Abramov qui explore les minuteries et comment pour contourner ce problème. C’est une lecture intéressante et une compréhension plus approfondie. Un problème est que les tableaux de dépendances vides peuvent souvent introduire des bogues dans notre code React. Il existe également un plug-in eslint que je recommanderais d’utiliser pour vous aider à les identifier. La documentation React met également en évidence les risques potentiels de l'utilisation du tableau de dépendances vide.

Une façon de corriger notre Timer serait de mettre à jour le tableau de dépendances pour l'effet. Cela signifierait que notre timerRef serait mis à jour à chaque intervalle. Cependant, il introduit le problème de la précision de la dérive.

 useEffect (() => {
  timerRef.current = setInterval (
 () => setInternalTime (internalTime - intervalle),
    intervalle
 )
  return () => {
 clearInterval (timerRef.current)
  }
}, [internalTime, interval])

Si vous vérifiez cette démo, elle a le même Timer deux fois avec des intervalles différents et enregistre la dérive dans la console du développeur. Un intervalle plus petit ou un temps plus long équivaut à une plus grande dérive.

Voir le stylo [3. Checking Timer Drift] (https://codepen.io/smashingmag/pen/zYZGRbN) par @ jh3y .

Voir le Stylo 3. Vérification de la dérive de la minuterie par @ jh3y .

Nous pouvons utiliser une réf pour résoudre notre problème. Nous pouvons l'utiliser pour suivre internalTime et éviter d'exécuter l'effet à chaque intervalle.

 const timeRef = useRef (time)
useEffect (() => {
  timerRef.current = setInterval (
    () => setInternalTime ((timeRef.current - = intervalle)),
    intervalle
  )
  return () => {
    clearInterval (timerRef.current)
  }
}, [interval])

Et cela réduit considérablement la dérive avec des intervalles plus petits également. Les minuteries sont une sorte de cas extrême. Mais c’est un bon exemple de penser à comment nous utilisons les crochets dans React . C'est un exemple qui m'est resté et qui m'a aidé à comprendre le «Pourquoi?».

Mettez à jour le rendu pour diviser le temps par 1000 et ajoutez un s et nous avons une seconde timer.

Voir le stylo [4. Rudimentary Timer] (https://codepen.io/smashingmag/pen/oNZXEVp) par @ jh3y .

Voir le stylo 4. Minuterie rudimentaire par @ jh3y .

Cette minuterie est encore rudimentaire. Il dérivera avec le temps. Pour notre jeu, tout ira bien. Si vous voulez creuser dans compteurs précis voici une superbe vidéo sur la création de chronomètres précis avec JavaScript.

Scoring

Permettons de mettre à jour la partition. Comment marquons-nous? En frappant une taupe! Dans notre cas, cela signifie cliquer sur un bouton . Pour l'instant, attribuons à chaque taupe un score de 100 et nous pouvons passer un rappel onWhack à notre Mole s.

 const MOLE_SCORE = 100

const Mole = ({onWhack}) => (
  
)

const Score = ({value}) => 
{`Score: $ {value}`}
const Jeu = () => { const [playing, setPlaying] = useState (faux) const [score, setScore] = useState (0) const onWhack = points => setScore (score + points) revenir ( {! jouer &&

Whac-A-Mole

} {en jouant && setPlaying (false)} /> }
) }

Notez comment le rappel onWhack est transmis à chaque Mole et que le rappel met à jour notre état score . Ces mises à jour déclencheront un rendu.

C'est le bon moment pour installer l'extension React Developer Tools dans votre navigateur. Il existe une fonctionnalité intéressante qui mettra en évidence les rendus de composants dans le DOM. Ouvrez l'onglet «Composants» dans les outils de développement et appuyez sur le rouage Paramètres. Sélectionnez «Mettre en surbrillance les mises à jour lors du rendu des composants»:

 Configuration de React DevTools
Configuration de React DevTools ( Grand aperçu )

Ouvrez la démo à ce lien et définissez l'extension pour mettre en évidence les rendus. Ensuite, vous verrez que le minuteur est rendu à mesure que le temps change mais lorsque nous frappons une taupe, tous les composants sont rendus.

Boucles dans JSX

Vous pensez peut-être que la façon dont nous rendre nos Mole s est inefficace. Et tu as raison de penser ça! Il y a une opportunité pour nous ici de les rendre en boucle .

Avec JSX, nous avons tendance à utiliser Array.map 99% du temps pour rendre une collection de choses. Par exemple :

 const USERS = [
  { id: 1, name: 'Sally' },
  { id: 2, name: 'Jack' },
]
const App = () => (
  
    {USERS.map (({id, name}) =>
  • {name}
  • )}
)

L'alternative serait de générer le contenu dans une boucle for puis de rendre le retour d'une fonction.

 return (
  
    {getLoopContent (DATA)}
)

À quoi sert cet attribut clé ? Cela aide React à déterminer quels changements doivent être rendus . Si vous pouvez utiliser un identifiant unique, faites-le! En dernier recours, utilisez l'index de l'élément dans une collection. (Lisez la documentation sur les listes pour en savoir plus.)

Pour notre exemple, nous ne disposons d'aucune donnée sur laquelle travailler. Si vous avez besoin de générer une collection d'éléments, voici une astuce que vous pouvez utiliser:

 new Array (NUMBER_OF_THINGS) .fill (). Map ()

Cela pourrait fonctionner pour vous dans certains scénarios.

 return (
  
    

Whac-A-Mole

{en jouant && console.info ('Terminé')} /> {nouveau Array (5) .fill (). map ((_, id) => )} }
)

Ou, si vous voulez une collection persistante, vous pouvez utiliser quelque chose comme uuid :

 import {v4 as uuid} from 'https://cdn.skypack.dev/uuid'
const MOLE_COLLECTION = new Array (5) .fill (). map (() => uuid ())

// Dans notre JSX
{MOLE_COLLECTION.map ((id) =>
  
)}

Fin de partie

Nous ne pouvons terminer notre partie qu'avec le bouton Démarrer. Lorsque nous le terminons, le score reste lorsque nous recommencons. Le onEnd pour notre Timer ne fait rien pour le moment.

Voir le stylo [6. Looping Moles] (https://codepen.io/smashingmag/pen/BaWNYEE) par @ jh3y .

Voir le stylo 6. Looping Moles par @ jh3y .

Ce dont nous avons besoin, c'est d'un troisième état où nous ne jouons pas mais nous avons terminé. Dans les applications plus complexes, je vous recommande d’atteindre XState ou à l’aide de réducteurs . Mais, pour notre application, nous pouvons introduire une nouvelle variable d'état: done . Lorsque l'état est ! Lecture et terminé nous pouvons afficher le score, réinitialiser le chronomètre et donner la possibilité de redémarrer.

Nous devons mettre nos limites logiques maintenant . Si nous terminons le jeu, alors au lieu de basculer en jouant nous devons également basculer terminé . Nous pourrions créer une fonction endGame et startGame .

 const endGame = () => {
  setPlaying (faux)
  setFinished (vrai)
}

const startGame = () => {
  setScore (0)
  setPlaying (vrai)
  setFinished (faux)
}

When we start a game, we reset the score and put the game into the playing state. This triggers the playing UI to render. When we end the game, we set finished to true. (The reason we don’t reset the score is so we can show it as a result.)

And, when our Timer ends, it should invoke that same function.


It can do that within an effect. If the internalTime hits 0then unmount and invoke onEnd.

useEffect(() => {
  if (internalTime === 0 && onEnd) {
    onEnd()
  }
}, [internalTime, onEnd])

We can shuffle our UI rendering to render three states:


  {!playing && !finished &&
    
      

Whac-A-Mole

} {playing && {new Array(NUMBER_OF_MOLES).fill().map((_, index) => ( ))} } {finished && }

And now we have a functioning game minus moving moles:

See the Pen [7. Ending a Game](https://codepen.io/smashingmag/pen/abJOqrw) by @jh3y.

See the Pen 7. Ending a Game by @jh3y.

Note how we’ve reused the Score component.
Was there an opportunity there to not repeat Score? Could you put it in its own conditional? Or does it need to appear there in the DOM. This will come down to your design.

Might you end up with a more generic component to cover it? These are the questions to keep asking. The goal is to keep a separation of concerns with your components. But, you also want to keep portability in mind.

Moles

Moles are the centerpiece of our game. They don’t care about the rest of the app. But, they’ll give you their score onWhack. This emphasizes portability.

We aren’t digging into styling in this “Guide”, but for our moles, we can create a container with overflow: hidden that our Mole (button) moves in and out of. The default position of our Mole will be out of view:

Mole Design
Mole design (Large preview)

We’re going to bring in a third-party solution to make our moles bob up and down. This is an example of how to bring in third-party solutions that work with the DOM. In most cases, we use refs to grab DOM elements, and then we use our solution within an effect.

We’re going to use GreenSock(GSAP) to make our moles bob. We won’t dig into the GSAP APIs today, but if you have any questions about what they’re doing, please ask me!

Here’s an updated Mole with GSAP:

import gsap from 'https://cdn.skypack.dev/gsap'

const Mole = ({ onWhack }) => {
  const buttonRef = useRef(null)
  useEffect(() => {
    gsap.set(buttonRef.current, { yPercent: 100 })
    gsap.to(buttonRef.current, {
      yPercent: 0,
      yoyo: true,
      repeat: -1,
    })
  }, [])
  return (
    
) }

We’ve added a wrapper to the button which allows us to show/hide the Moleand we’ve also given our button a ref. Using an effect, we can create a tween (GSAP animation) that moves the button up and down.

You’ll also notice that we’re using className which is the attribute equal to class in JSX to apply class names. Why don’t we use the className with GSAP? Because if we have many elements with that classNameour effect will try to use them all. This is why useRef is a great choice to stick with.

See the Pen [8. Moving Moles](https://codepen.io/smashingmag/pen/QWpbQXW) by @jh3y.

See the Pen 8. Moving Moles by @jh3y.

Awesome, now we have bobbing Moles, and our game is complete from a functional sense. They all move exactly the same which isn’t ideal. They should operate at different speeds. The points scored should also reduce the longer it takes for a Mole to get whacked.

Our Mole’s internal logic can deal with how scoring and speeds get updated. Passing the initial speeddelayand points in as props will make for a more flexible component.


Now, for a breakdown of our Mole logic.

Let’s start with how our points will reduce over time. This could be a good candidate for a ref. We have something that doesn’t affect render whose value could get lost in a closure. We create our animation in an effect and it’s never recreated. On each repeat of our animation, we want to decrease the points value by a multiplier. The points value can have a minimum value defined by a pointsMin prop.

const bobRef = useRef(null)
const pointsRef = useRef(points)

useEffect(() => {
  bobRef.current = gsap.to(buttonRef.current, {
    yPercent: -100,
    duration: speed,
    yoyo: true,
    repeat: -1,
    delay: delay,
    repeatDelay: delay,
    onRepeat: () => {
      pointsRef.current = Math.floor(
        Math.max(pointsRef.current * POINTS_MULTIPLIER, pointsMin)
      )
    },
  })
  return () => {
    bobRef.current.kill()
  }
}, [delay, pointsMin, speed])

We’re also creating a ref to keep a reference for our GSAP animation. We will use this when the Mole gets whacked. Note how we also return a function that kills the animation on unmount. If we don’t kill the animation on unmount, the repeat code will keep firing.

See the Pen [9. Score Reduction](https://codepen.io/smashingmag/pen/JjWdpQr) by @jh3y.

See the Pen 9. Score Reduction by @jh3y.

What will happen when a mole gets whacked? We need a new state for that.

const [whacked, setWhacked] = useState(false)

And instead of using the onWhack prop in the onClick of our buttonwe can create a new function whack. This will set whacked to true and call onWhack with the current pointsRef value.

const whack = () => {
 setWhacked(true)
 onWhack(pointsRef.current)
}

return (
 
)

The last thing to do is respond to the whacked state in an effect with useEffect. Using the dependency arraywe can make sure we only run the effect when whacked changes. If whacked is truewe reset the points, pause the animation, and animate the Mole underground. Once underground, we wait for a random delay before restarting the animation. The animation will start speedier using timescale and we set whacked back to false.

useEffect(() => {
  if (whacked) {
    pointsRef.current = points
    bobRef.current.pause()
    gsap.to(buttonRef.current, {
      yPercent: 100,
      duration: 0.1,
      onComplete: () => {
        gsap.delayedCall(gsap.utils.random(1, 3), () => {
          setWhacked(false)
          bobRef.current
            .restart()
            .timeScale(bobRef.current.timeScale() * TIME_MULTIPLIER)
        })
      },
    })
  }
}, [whacked])

That gives us:

See the Pen [10. React to Whacks](https://codepen.io/smashingmag/pen/MWpwQNy) by @jh3y.

See the Pen 10. React to Whacks by @jh3y.

The last thing to do is pass props to our Mole instances that will make them behave differently. But, how we generate these props could cause an issue.

{new Array(MOLES).fill().map((_, id) => ( ))}

This would cause an issue because the props would change on every render as we generate the moles. A better solution could be to generate a new Mole array each time we start the game and iterate over that. This way, we can keep the game random without causing issues.

const generateMoles = () => new Array(MOLES).fill().map(() => ({
  speed: gsap.utils.random(0.5, 1),
  delay: gsap.utils.random(0.5, 4),
  points: MOLE_SCORE
}))
// Create state for moles
const [moles, setMoles] = useState(generateMoles())
// Update moles on game start
const startGame = () => {
  setScore(0)
  setMoles(generateMoles())
  setPlaying(true)
  setFinished(false)
}
// Destructure mole objects as props
{moles.map(({speed, delay, points}, id) => ( ))}

And here’s the result! I’ve gone ahead and added some styling along with a few varieties of moles for our buttons.

See the Pen [11. Functioning Whac-a-Mole](https://codepen.io/smashingmag/pen/VwpLQod) by @jh3y.

See the Pen 11. Functioning Whac-a-Mole by @jh3y.

We now have a fully working “Whac-a-Mole” game built in React. It took us less than 200 lines of code. At this stage, you can take it away and make it your own. Style it how you like, add new features, and so on. Or you can stick around and we can put together some extras!

Tracking The Highest Score

We have a working “Whac-A-Mole”, but how can we keep track of our highest achieved score? We could use an effect to write our score to localStorage every time the game ends. But, what if persisting things was a common need. We could create a custom hook called usePersistentState. This could be a wrapper around useState that reads/writes to localStorage.

  const usePersistentState = (key, initialValue) => {
  const [state, setState] = useState(
    window.localStorage.getItem(key)
      ? JSON.parse(window.localStorage.getItem(key))
      : initialValue
  )
  useEffect(() => {
    window.localStorage.setItem(key, state)
  }, [key, state])
  return [state, setState]
}

And then we can use that in our game:

const [highScore, setHighScore] = usePersistentState('whac-high-score', 0)

We use it exactly the same as useState. And we can hook into onWhack to set a new high score during the game when appropriate:

const endGame = points => {
  if (score > highScore) setHighScore(score) // play fanfare!
}

How might we be able to tell if our game result is a new high score? Another piece of state? Most likely.

See the Pen [12. Tracking High Score](https://codepen.io/smashingmag/pen/NWpqYKK) by @jh3y.

See the Pen 12. Tracking High Score by @jh3y.

Whimsical Touches

At this stage, we’ve covered everything we need to. Even how to make your own custom hook. Feel free to go off and make this your own.

Sticking around? Let’s create another custom hook for adding audio to our game:

const useAudio = (src, volume = 1) => {
  const [audio, setAudio] = useState(null)
  useEffect(() => {
    const AUDIO = new Audio(src)
    AUDIO.volume = volume
    setAudio(AUDIO)
  }, [src])
  return {
    play: () => audio.play(),
    pause: () => audio.pause(),
    stop: () => {
      audio.pause()
      audio.currentTime = 0
    },
  }
}

This is a rudimentary hook implementation for playing audio. We provide an audio src and then we get back the API to play it. We can add noise when we “whac” a mole. Then the decision will be, is this part of Mole? Is it something we pass to Mole? Is it something we invoke in onWhack ?

These are the types of decisions that come up in component-driven development. We need to keep portability in mind. Also, what would happen if we wanted to mute the audio? How could we globally do that? It might make more sense as a first approach to control the audio within the Game component:

// Inside Game
const { play: playAudio } = useAudio('/audio/some-audio.mp3')
const onWhack = () => {
  playAudio()
  setScore(score + points)
}

It’s all about design and decisions. If we bring in lots of audio, renaming the play variable could get tedious. Returning an Array from our hook-like useState would allow us to name the variable whatever we want. But, it also might be hard to remember which index of the Array accounts for which API method.

See the Pen [13. Squeaky Moles](https://codepen.io/smashingmag/pen/eYvNMOB) by @jh3y.

See the Pen 13. Squeaky Moles by @jh3y.

That’s It!

More than enough to get you started on your React journey, and we got to make something interesting. We sure did cover a lot:

  • Creating an app,
  • JSX,
  • Components and props,
  • Creating timers,
  • Using refs,
  • Creating custom hooks.

We made a game! And now you can use your new skills to add new features or make it your own.

Where did I take it? At the time of writing, it’s at this stage so far:

See the Pen [Whac-a-Mole w/ React && GSAP](https://codepen.io/smashingmag/pen/JjWdLPO) by @jh3y.

See the Pen Whac-a-Mole w/ React && GSAP by @jh3y.

Where To Go Next!

I hope building “Whac-a-Mole” has motivated you to start your React journey. Where next? Well, here are some links to resources to check out if you’re looking to dig in more — some of which are ones I found useful along the way.

Smashing Editorial" width="35" height="46" loading="lazy" decoding="async(vf, il)




Source link