Commencez avec React en créant un jeu WhacAMole
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 .
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 .
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 .
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 .
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 = () =>
render ( document.getElementById ('app'))
Voir le stylo [An Updating Timer] (https://codepen.io/smashingmag/pen/MWpwQqa) par @ 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 (
{`Intervalle: $ {interval}`}
updateInterval (e.target.value)} />
)
}
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 .
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

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 .
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:
- Le compte à rebours;
- À quoi intervalle qu'il va mettre à jour;
- 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 .
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 .
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 .
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»:

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 .
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 0
then 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.
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:

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 Mole
and 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 className
our 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.
Awesome, now we have bobbing Mole
s, 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 speed
delay
and 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.
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 button
we 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 true
we 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.
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.
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.
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.
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.
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.

Source link