Create a Tenzies Game using ReactJS
An implementation of Tenzies Game in ReactJS. While creating this project I learned about Event Listeners in React, React State, Conditional Rendering in React, React Hooks(useEffect), etc. A player could track the number of rolls, current time and best time it took to win the game. Have Fun 😄. After creating the project, it was deployed to GitHub Pages 🐦 Feel free to reach me onTwitter 👾
- ReactJS
- create-react-app
- Figma Design Template
- Event Listeners in React
- React State
- Conditional Rendering in React
- React Hooks(useEffect)
- github-pages
cd tenzies
npm install
npm start
- Initialize the project using
npx create-react-app tenzieswhich will create a complete React App pre-configured and pre-installed with all the dependencies. - Import
Karlafont from google fonts and apply it to theAppcomponent.
- Create a
componentsfolder inside thesrcdirectory. - Create custom components inside the
componentsfolder. - Create a
stylesfolder inside thesrcdirectory and add.cssfiles inside it.
- Delete unnecessary files and code from the directory.
-
Create a
Appcomponent and basic JSX elements for it. -
Add appropriate
classNames to elements in theAppcomponent. -
Import
Appcomponent insideindex.js. Code insideindex.jslooks like this :-import React from "react"; import ReactDOM from "react-dom/client"; import "./styles/index.css"; import App from "./App"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( <React.StrictMode> <App /> </React.StrictMode> );
-
Add these styles to
index.css:-body { margin: 0; background-color: #0b2434; } * { box-sizing: border-box; }
-
Style
Appcomponent by editingApp.cssand add these styles :-.App { font-family: "Karla", sans-serif; text-align: center; display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100vh; } main { background-color: #f5f5f5; height: 40em; width: 40em; border-radius: 10px; box-shadow: rgba(254, 254, 254, 0.25) 0px 13px 27px -5px, rgba( 255, 255, 255, 0.3 ) 0px 8px 16px -8px; }
-
Create a
Dicecomponent and basic JSX elements for it. -
Add appropriate
classNames to elements in theDicecomponent.-
Update code inside
App.jsand it should look like this :-import "./styles/App.css"; import Dice from "./components/Dice"; import Footer from "./components/Footer"; function App() { function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(Math.ceil(Math.random() * 6)); } return newDice; } console.log(allNewDice()); return ( <div className="App"> <main> <div className="dice-container"> <Dice value="1" /> <Dice value="2" /> <Dice value="3" /> <Dice value="4" /> <Dice value="5" /> <Dice value="6" /> <Dice value="1" /> <Dice value="2" /> <Dice value="3" /> <Dice value="4" /> </div> </main> <Footer /> </div> ); } export default App;
-
Code inside
Dice.jslooks like this :-
function Dice(props) { return ( <div className="dice-face"> <h2 className="dice-num">{props.value}</h2> </div> ); } export default Dice;
-
-
Style
Dicecomponent by editingApp.cssand add these styles :-main { background-color: #f5f5f5; height: 40em; width: 40em; border-radius: 10px; box-shadow: rgba(254, 254, 254, 0.25) 0px 13px 27px -5px, rgba( 255, 255, 255, 0.3 ) 0px 8px 16px -8px; padding: 20px; display: flex; flex-direction: column; justify-content: center; align-items: center; } .dice-container { display: grid; grid-template: auto auto / repeat(5, 1fr); gap: 20px; } /* Dice Component */ .dice-face { height: 50px; width: 50px; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15); border-radius: 10px; display: flex; justify-content: center; align-items: center; cursor: pointer; background-color: white; } .dice-num { font-size: 2rem; } /* Dice Component */
- Create
Footercomponent and basic JSX elements for it. - Import
Footercomponent insideAppcomponent. - Style
Footercomponent.
- Write a
allNewDicefunction that returns an array of 10 random numbers between 1-6 inclusive. - Log the array of numbers to the console for now.
- Code for
allNewDicefunction insideAppcomponent looks like this :-function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(Math.ceil(Math.random() * 6)); } return newDice; } console.log(allNewDice());
-
Put Real Dots on the Dice. Here is a link to an article that helped me with some of the css in
Dicecomponent Creating Dice in Flexbox in CSS -
Update styles for
Dicecomponent inApp.cssand it should look like this :-/* Dice Component */ .dice-face { height: 55px; width: 55px; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.15); border-radius: 10px; display: flex; justify-content: center; /* align-items: center; */ cursor: pointer; background-color: white; padding: 12%; } /* .dice-num { font-size: 2rem; } */ .dot { display: block; width: 12px; height: 12px; border-radius: 50%; background-color: rgb(50, 50, 50); } .dice { width: 2.5em; } .first-face { display: flex; justify-content: center; align-items: center; } .second-face, .third-face, .fourth-face, .fifth-face, .sixth-face { display: flex; justify-content: space-between; } .second-face .dot:nth-of-type(2), .third-face .dot:nth-of-type(3) { align-self: flex-end; } .third-face .dot:nth-of-type(1) { align-self: flex-start; } .third-face .dot:nth-of-type(2), .fifth-face .column:nth-of-type(2) { align-self: center; } .fourth-face .column, .fifth-face .column { display: flex; flex-direction: column; justify-content: space-between; } /* Dice Component */
-
Update code for
Dicecomponent inDice.jsand it should look like this :-function Dice(props) { const diceValue = parseInt(props.value); let diceSpanEles; if (diceValue === 1) { diceSpanEles = ( <div className="dice first-face"> <span className="dot" style={{ backgroundColor: "rgb(255 100 89)" }} > {" "} </span> </div> ); } else if (diceValue === 2) { diceSpanEles = ( <div className="dice second-face"> <span className="dot"> </span> <span className="dot"> </span> </div> ); } else if (diceValue === 3) { diceSpanEles = ( <div className="dice third-face"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> ); } else if (diceValue === 4) { diceSpanEles = ( <div className="fourth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else if (diceValue === 5) { diceSpanEles = ( <div className="fifth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else if (diceValue === 6) { diceSpanEles = ( <div className="fourth-face dice"> <div className="column"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> <div className="column"> <span className="dot"></span> <span className="dot"></span> <span className="dot"></span> </div> </div> ); } else { diceSpanEles = <h2 className="die-num">{props.value}</h2>; } return <div className="dice-face">{diceSpanEles}</div>; } export default Dice;
-
Import
useStatehook from react using :-import { useState } from "react";
-
Create a state inside
Appcomponent to hold our array of numbers(Initialize the state by calling ourallNewDicefunction so it loads all new(random) dice as soon as the app loads). :-const [dice, setDice] = useState(allNewDice());
-
Map over the state numbers array to generate our array of
diceElementsand render those in place of our manually-written 10 Dice elements.const diceElements = dice.map((dice) => <Dice value={dice} />);
-
React will show the following warning, we will fix it in the future(Ignore this for now)
Warning ⚠️ : Each child in a list should have a unique "key" prop.
-
Create a
Rolldice button insideAppcomponent that will re-roll all 10 dice.<button className="roll-dice" onClick="{rollDice}">Roll</button>
-
Clicking the
Rolldice button runsrollDice()function, which should generate a new array of numbers and set thedicestate to that new array (thus re-rendering the array to the page).function rollDice() { setDice(allNewDice()); }
-
Style
Rolldice button using styles from figma design template. Add these styles toApp.css:-.roll-dice { margin-top: 2em; height: 50px; width: 150px; border: none; border-radius: 6px; background-color: #5035ff; color: white; font-size: 1.2rem; font-family: "Karla", sans-serif; cursor: pointer; } .roll-dice:focus { outline: none; } .roll-dice:active { box-shadow: inset 5px 5px 10px -3px rgba(0, 0, 0, 0.7); }
-
Inside
Appcomponent, update the array of numbers in state to be an array of objects instead. Each object should look like:{ value: <random number>, isHeld: false }. UpdatedallNewDice()function looks something like this :-function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push({ value: Math.ceil(Math.random() * 6), isHeld: false, }); } return newDice; }
-
Making this change will break parts of our code, so we need to update
diceElementvariable and access thevaluekey from ourarray of objects. UpdateddiceElementsvariable looks something like this :-const diceElements = dice.map((dice) => <Dice value={dice.value} />);
-
Let's fix this warning ->
Warning ⚠️ : Each child in a list should have a unique "key" prop., by using a npm packagenanoidwhich lets us generate unique ID's on the fly. Here are the code changes we need to make to theAppcomponent to make this work :-- Import
nanoidpackage at the top ofApp.js:
import { nanoid } from "nanoid";
- Create an
idproperty and assignnanoid()function as it's value :
value: Math.ceil(Math.random() * 6), isHeld: false, id: nanoid()
- Assign the
keyprop the value ofid:
const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} /> ));
- Import
-
Pass a
isHeldprop insideAppcomponent, indiceElementwhen rendering ourDicecomponent.const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} isHeld={dice.isHeld} /> ));
-
Add conditional styling to the
Dicecomponent so that if it's isheld prop is true(isHeld === true), its background color changes to a light green(#59E391).const styles = { backgroundColor: props.isHeld ? "#59E391" : "white", }; return ( <div className="dice-face" style={styles}> {diceSpanEles} </div> );
-
In
Appcomponent, create a functionholdDicethat takesidas a parameter. For now, just have the function console.log(id). Pass that function down to each instance of the Die component as a prop, so when each one is clicked, it logs its own unique ID property.function holdDice(id) { console.log(id); } const diceElements = dice.map((dice) => ( <Dice key={dice.id} value={dice.value} isHeld={dice.isHeld} holdDice={() => holdDice(dice.id)} /> ));
-
In
Dicecomponent acceptholdDiceprop and bound it toonClickevent.<div className="dice-face" style={styles} onClick={props.holdDice}> {diceSpanEles} </div>
-
Update the
holdDicefunction to flip theisHeldproperty on the object in the array that was clicked, based on theidprop passed into the function. InAppcomponent, we will usesetDicestate function then.map()over the array of objects. Every Dice object will be in exactly the same state as it was, except the ones that has their isHeld property flipped(to true).function holdDice(id) { setDice((oldDice) => oldDice.map((dice) => { return dice.id === id ? { ...dice, isHeld: !dice.isHeld } : dice; }) ); }
-
Create a helper function
generateNewDice()that allows us to generate newDiceobject, when we call it. Let's use helper function to createDiceobject insideallNewDicefunction.function generateNewDice() { return { value: Math.ceil(Math.random() * 6), isHeld: false, id: nanoid(), }; } function allNewDice() { const newDice = []; for (let i = 0; i < 10; i++) { newDice.push(generateNewDice()); } return newDice; }
-
Update the
rollDicefunction to not just roll all new dice, but instead to look through the existing dice to NOT roll any dice that are beingheld. Same asholdDicefunction, we will usesetDicestate function then.map()over the array of objects. When calling helper functiongenerateNewDice(), every Dice object's value will be changed, except the ones that has their property isHeld === true.function holdDice(id) { setDice((oldDice) => oldDice.map((dice) => { return dice.id === id ? { ...dice, isHeld: !dice.isHeld } : dice; }) ); }
-
Add Title & Description elements to give some additional information to the users. Style
<h1 className="title">Tenzies</h1> <p className="instructions"> Roll until all dice are the same. Click each die to freeze it at its current value between rolls. </p>
.title { font-size: 40px; margin: 0; } .instructions { font-family: "Inter", sans-serif; font-weight: 400; margin-top: 0; text-align: center; margin-top: 1em; }
-
In
Appcomponent add new state calledtenzies, default to false. It represents whether the user has won the game yet or not.const [tenzies, setTenzies] = useState(false);
-
Add an Effect Hook
(useEffect)that runs every time thedicestate array changes. For now, just console.log("Dice state changed"). We are using effect hook(useEffect)in order to keep two states(Dice & tenzies)in sync with each other. Ignore thenon-unused-varswarnings for now.import { useState, useEffect } from "react"; useEffect(() => { console.log("Dice state changed"); }, [dice]);
-
We will use
.every()array method, which returnstrueif every item in the array is same else it returnsfalse. In our case if all dice are held & all dice have the same value,console.log("You won!")Let's update our Effect Hook(useEffect)->useEffect(() => { // All dice are held const allHeld = dice.every((die) => die.isHeld); // All dice have the same value const firstValue = dice[0].value; const allSameValue = dice.every((die) => die.value === firstValue); // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { setTenzies(true); console.log("You won!"); } }, [dice]);
-
If tenzies is
true, change the button text to "New Game" and use thereact-confettipackage to render the component.npm install react-confettiimport Confetti from "react-confetti"; <main> {tenzies && <Confetti />} <button className="roll-dice" onClick={rollDice}> {tenzies ? "New Game" : "Roll"} </button> </main>;
-
Allow the user to play a new game when the
New Gamebutton is clicked and they've already won. InAppcomponent, let's updaterollDice()function such that user can only roll the dice iftenzies === false. Elsetenzies === true(if they've won the game), settenzies === falseand generate all new dice.function rollDice() { if (!tenzies) { setDice((oldDice) => oldDice.map((dice) => { return dice.isHeld ? dice : generateNewDice(); }) ); } else { setTenzies(false); setDice(allNewDice()); } }
-
Track the number of Rolls it took to win the game. Inside
Appcomponent, let's define a state callednumOfRollsand set it's default value to0.const [numOfRolls, setNumOfRolls] = useState(0);
-
Inside
rollDice()function add a couple of statements that changenumOfRollsstate, such that whenRollbutton is clicked (game is not won) it increasesnumOfRollsstate by 1. And when game is won andNew Gamebutton is clicked (game is won),numOfRollsstate is reset back to 0.function rollDice() { if (!tenzies) { setNumOfRolls((prevState) => prevState + 1); } else { setNumOfRolls(0); } }
-
Create
<h2>element and insert value ofnumOfRollsstate inside it.<h2 className="track-rolls">Number of Rolls: {numOfRolls}</h2>
-
Track the time it took to win the game. In
Appcomponent initiate two states[time],[running]and set their default states to0,falserespectively.[time]representing the recorded time and[running]as if the game is being played or is won.const [time, setTime] = useState(0); const [running, setRunning] = useState(false);
-
Calculate time using
useEffectHook &setInterval()method. Follow this article for detailed information.useEffect(() => { let interval; if (running) { interval = setInterval(() => { setTime((prevTime) => prevTime + 10); }, 10); } else if (!running) { clearInterval(interval); } return () => clearInterval(interval); }, [running]);
-
Update the
useEffectHook, that represents game state. Using this hook Start or Stop thetimer.// useEffect Hook that represents game state useEffect(() => { // Check if some Dice are held(even if it's just one) const someHeld = dice.some((die) => die.isHeld); // if `someHeld` === True, Start counting if (someHeld) { setRunning(true); } // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { // Stop Counter setRunning(false); // Game Won setTenzies(true); } }, [dice]);
-
Update
rollDice()function such that if game is won, reset the counter whenNew Gamebutton is clicked.function rollDice() { if (!tenzies) { //... } else { // Reset timer setTime(0); } }
-
Create JSX elements that will hold values for
minutes,seconds,milliseconds.<h3> <div className="timer"> <div className="current-time"> <span> {("0" + Math.floor((time / 60000) % 60)).slice(-2)}: </span> <span>{("0" + Math.floor((time / 1000) % 60)).slice(-2)}:</span> <span>{("0" + ((time / 10) % 100)).slice(-2)}</span> </div> </div> </h3>
-
Save Best Time to
localStorageand try to beat the record. InsideAppcomponent initiate a state[bestTime]and set it's default value to23450(just a random value).const [bestTime, setBestTime] = useState(23450);
-
Using
useEffectHook that getsbestTimefrom localStorage . Follow this article for detailed instructions.useEffect(() => { const bestTime = JSON.parse(localStorage.getItem("bestTime")); if (bestTime) { setBestTime(bestTime); } }, []);
-
Update the
useEffectHook, that represents game state. Using this hook store thecurrentTimein localStorageif(currentTime < bestTime)and also make changes to the dependency array( addtime,bestTimeto it ).// useEffect Hook that represents game state useEffect(() => { // ... // if `allHeld` and `allSameValue)` === true, we won if (allHeld && allSameValue) { // ... // Store Time at the end of a win in a variable let currentTime = time; // if currentTime > bestTime, store it in localStorage if (currentTime < bestTime) { setBestTime(currentTime); localStorage.setItem("bestTime", JSON.stringify(currentTime)); } // ... } }, [dice, time, bestTime]);
-
Create JSX elements that will hold
minutes,seconds,millisecondsvalues forbestTime. Also, add some styling totimerdiv.<div className="timer"> <div className="current-time"> <!-- ... --> </div> <div className="best-time"> <h3 className="best">Best</h3> <div> <span> {( "0" + Math.floor((bestTime / 60000) % 60) ).slice(-2)} : </span> <span> {( "0" + Math.floor((bestTime / 1000) % 60) ).slice(-2)} : </span> <span> {("0" + ((bestTime / 10) % 100)).slice(-2)} </span> </div> </div> </div>
Styles ->
.timer { display: flex; justify-content: space-around; width: 25vw; } .timer h3 { margin: 10px; }
-
Change Absolute units to Relative.
-
Make App responsive for mobile by adding
media query. 😃
-
Delete unnecessary files from directory and format code with
Prettier. -
Test for Responsiveness and make changes if need be.
-
Add links to
Live Previewand screenshots.
- Use Official Documentation(link) to push the project to GitHub Pages 🎆🎆🎆
- CSS - Put Real Dots on the Dice. ✅
- JS - Track Number of Rolls it took to win the game. ✅
- JS - Track the time it took to win the game. ✅
- JS - Save Best Time/Rolls to
localStorageand try to beat the record. ✅
-
The Odin Project
-
Figma Design
-
Scrimba
-
React Official Documentation
“Humans are allergic to change. They love to say, ‘We’ve always done it this way.’ I try to fight that. That’s why I have a clock on my wall that runs counterclockwise.”
— Grace Hopper
♾️❇️🔥








