Lab 6: React
🎯 Lab ObjectiveIn this lab, you will learn React
This lab teaches the following concepts:
- Template Strings
- The
<template>
element- React Components and JSX
- React State
Table of Contents
String concatenation in JavaScript is simple, but the syntax can get messy…
let name = "Polly";
let country = "Antarctica";
let msg = "Welcome, " + name + ", to " + country + "!";
Introducing a cleaner syntax: Template Strings!
let msg = `Welcome, ${name}, to ${country}!`;
✅ Tip
Template strings contain placeholders ${ }
which are substituted with values. Placeholders may contain any JavaScript expression including variables and data fields:
console.log(`π is about ${22 / 7}`);
✅ TipMany mainstream programming languages have an equavalent of template strings.
Python 3 (from version 3.6 onwards) has F-Strings:
msg = f'Welcome, {name}, to {country}!!'
Java (from version 21 onwards) template processors:
msg = STR."Welcome, \{name}, to \{country}!!";
Template strings respect line breaks, so we can use them to create templates for HTML!
const html = `
<div className="card">
<h2>Welcome, ${name}, to ${country}!</h2>
<img src="${img}" className="profile" />
</div>
`;
document.querySelector('main').innerHTML = html;
📝 Task 1: Template StringsA simple To-Do List application
- Create a new folder for this project
- Copy the code below into a
.html
file- Replace
const time = ...
with a Template String- Replace
item.innerHTML =
with a Template String- Test the functionality
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Lab 6 Templating</title> <style> body { display: flex; flex-direction: column; align-items: center; } main.listClass { width: 90%; padding: 10px; } main>div.itemClass { display: flex; justify-content: space-between; padding: 10px; } main, main>div.itemClass { border: 2px solid gray; border-radius: 10px; margin: 10px; } .timeClass { color: gray; font-size: 0.8em; } </style> <script> function submit() { // get input const input = document.querySelector('input') const text = input.value // format timestamp string const d = new Date(); const time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds() + "." + d.getMilliseconds() // create new element const item = document.createElement('div') item.setAttribute('class', 'itemClass'); item.innerHTML = '<input type="checkbox" /><span class="textClass">' + text + '</span><span class="timeClass">' + time + '</span>'; // append new element document.querySelector('main.listClass').append(item) } </script> </head> <body> <h1>Todo List</h1> <div> <input type="text"> <button onclick="submit()">Add Item</button> </div> <main class="listClass"> <p>Items:</p> </main> </body> </html>
The HTML <template>
element can also be used to create DOM elements. First we define a <template>
element which contains the HTML we wish to create.
<template>
<div>
<h2></h2>
<img src="" />
</div>
</template>
Then we clone the template
// reference to the template
let temp = document.querySelector("template");
// clone the template
let clone = temp.content.cloneNode(true);
Insert our data into the cloned template
clone.querySelector('h2').innerText = 'Hello';
Then add it to the page
document.body.appendChild(clone);
📝 Task 2: Template Element
Copy the following
<template>
into the.html
file you made earlier.The
template
element can be placed pretty much anywere in an HTML file. Since it is a template and not actually part of the document, it makes the most sense to put it in thehead
section of the page below thescript
section.<template id="itemTemplate"> <div class="itemClass"> <input type="checkbox" /> <span class="textClass"></span> <span class="timeClass"></span> </div> </template>
- In the JavaScript, replace the
const item = ...
code with:// get a reference to the template const template = document.querySelector("#itemTemplate"); // clone the template const item = template.content.cloneNode(true); // insert some values item.querySelector('.textClass').textContent = text; item.querySelector('.timeClass').textContent = time; // append new element as usual
- Test the functionality
The two previous tasks (creating HTML with two different forms of templates) are both examples of 1-way data binding. We bind data to a template to create a view. This approach is limited, because, if/when data changes our view does not update. We have to manually re-bind the data, and regenerate the view… which is tedious…
React is a client-side framework which provides 2-way data binding. This means that React can synchronize the data and view to eachother. No more manual DOM manipulation!
Learning the syntax of React (or any of the other eleventy bazillion JavaScript frameworks that do the same thing) might be hard, but it’s worth it.
With React, we will build a simple card game based on Black Jack. In our game each card has a value. If the cards in our hand sum to 21 we win, if we exceed 21 we lose.
📝 Task 3: Create React AppReact expects a certain structure for the project. To create the project we will use the built tool
vite
.
- In a terminal (insider your
labs
folder) create a new project via:
npm create vite@latest
- (y) to install create-vite
- Project name: >
card-game
- Select a framework >
react
- Select a variant >
JavaScript
cd card-game
- Run
npm install
to download the React framework into the project- Start the dev server:
npm run dev
- Visit the hosted site http://localhost:5173/
- Open the project directory with VS Code
The two files we will be editing are src/App.jsx
and src/App.css
, they contain a React component we will be modifying for the card game. If you want to understand the purpose of the other files, read the list below (or refer back to it at your leisure)
package.json
- metadata on which node packages are installed
package-lock.json
- metadata on the exact version of each package that is currently installed
node_modules/
- contains the actual files for each package
- these files are generated, and should not be added to any Git repositories
.gitignore
- metadata for git to ignore certain files
- you will see that the
node_modules
directory is included in the ignore list
index.html
the file React inserts components intopublic/
root folder that is served as a websitevite.svg
– the Vite logo that is displayed on the default page
src/
the source code for the projectApp.jsx
a provided example componentApp.css
style for the above component (App.jsx
)index.css
global styles for the entire projectmain.jsx
the initial script which inserts components intoindex.html
assets/react.svg
asset which can be deleted
React components are JavaScript functions which return JSX. JSX is a syntax extension to JavaScript which provides a better way to template HTML. JSX was created as part of React, but some other JavaScript frameworks also use JSX.
In JSX, <tags>
which start with a lower case are HTML elements, and <Tags>
that begin with an uppercase are React Components.
Here is a React Component; as you can see it returns JSX (which looks like HTML).
// App.jsx
import React from 'react';
import './App.css';
function App() {
const score = 0;
return (
<div className="App">
<h1>Card Game</h1>
<p>Score: {score}</p>
</div>
);
}
export default App; // exported so index.js can load it
The function App()
is a React Component, it returns JSX. The returned JSX contains three elements. The <p>
element includes {score}
which will be substituted with the variable score.
📝 Task 4: Edit App.jsx
Replace entire the contents of
App.jsx
with the one provided above
- you may also delete
vite.svg
since it is no longer being usedSet score to different number
- save, reload, and check it updates
- this isn’t 2-way data binding yet…
To properly bind data to a component we have to use State
. When a component’s state updates it will trigger the component to reload it’s view.
We can declare a state variable with React.useState()
. Which returns a pair of values, the current state, and the function that updates it. We also pass in the initial value.
📝 Task 5: State Hook
- Replace
const score = 0;
with:const [score, setScore] = React.useState(0);
- Lets add a function that increases the score by 1
- put the function inside
function App()
- (a function declaration inside a function!!!)
function increaseScore() { setScore(score + 1); }
- Add a button to the template to call our
increaseScore()
function
- insert the button below the
<p>
element<button onClick={increaseScore}>Add Score</button>
- Test it
- everytime you click the button the view updates!!!
- Yeah! 2-Way Data Binding
Sometimes, we need to render different components based on some condition. We could use a regular JavaScript if-statement to decide which JSX component to render:
if (!isLoggedIn) {
return <LoginButton />;
} else {
return <LogoutButton />;
}
We could also achieve the same effect inline with the Logical &&
Operator
return (
{!isLoggedIn && <LoginButton />}
{isLoggedIn && <LogoutButton />}
);
It works because JavaScript does Short-circuit evaluation: For Logical And, both left and right need to be true, so if the left expression is false
it won’t evaluate the right expression. So the <Component />
will only render if the left expression is true
.
📝 Task 6: Conditional Rendering
- Show Win/Lose statements
- add the following code into your component inside the bottom of the
div
element{score === 21 && <h1>You won!</h1>} {score > 21 && <h1>You lost</h1>}
- Your
App.jsx
should look like this:function App() { const [score, setScore] = React.useState(0); function increaseScore() { setScore(score + 1); } return ( <div className="App"> <h1>Card Game</h1> <p>Score: {score}</p> <button onClick={increaseScore}>Add Score</button> {score === 21 && <h1>You won!</h1>} {score > 21 && <h1>You lost</h1>} </div> ); }
- Test the game out
- get to the winning state
- get to a losing state
We have provided some assets to make the game look nicer
📝 Task 7: Game Assets
- Replace the content of
App.css
with:.App { color: white; background: green; display: flex; flex-direction: column; align-items: center; min-height: 100vh; } .hand { display: flex; justify-content: center; width: 100vw; } .card { margin: 8px; max-width: 30%; min-width: 5%; }
- Download the card images here: cards.zip
- unzip the images into the
public/
folder- e.g.
public/cards/ace_of_spades.svg'
- Copy the below JSON array into
App.jsx
- (after the imports; before the
function App()
component)const cardData = [ { value: 1, img: './cards/ace_of_spades.svg'}, { value: 2, img: './cards/2_of_spades.svg'}, { value: 3, img: './cards/3_of_spades.svg'}, { value: 4, img: './cards/4_of_spades.svg'}, { value: 5, img: './cards/5_of_spades.svg'}, { value: 6, img: './cards/6_of_spades.svg'}, { value: 7, img: './cards/7_of_spades.svg'}, { value: 8, img: './cards/8_of_spades.svg'}, { value: 9, img: './cards/9_of_spades.svg'}, { value: 10, img: './cards/10_of_spades.svg'}, { value: 10, img: './cards/jack_of_spades2.svg'}, { value: 10, img: './cards/queen_of_spades2.svg'}, { value: 10, img: './cards/king_of_spades2.svg'}, { value: 1, img: './cards/ace_of_clubs.svg'}, { value: 2, img: './cards/2_of_clubs.svg'}, { value: 3, img: './cards/3_of_clubs.svg'}, { value: 4, img: './cards/4_of_clubs.svg'}, { value: 5, img: './cards/5_of_clubs.svg'}, { value: 6, img: './cards/6_of_clubs.svg'}, { value: 7, img: './cards/7_of_clubs.svg'}, { value: 8, img: './cards/8_of_clubs.svg'}, { value: 9, img: './cards/9_of_clubs.svg'}, { value: 10, img: './cards/10_of_clubs.svg'}, { value: 10, img: './cards/jack_of_clubs2.svg'}, { value: 10, img: './cards/queen_of_clubs2.svg'}, { value: 10, img: './cards/king_of_clubs2.svg'}, { value: 1, img: './cards/ace_of_hearts.svg'}, { value: 2, img: './cards/2_of_hearts.svg'}, { value: 3, img: './cards/3_of_hearts.svg'}, { value: 4, img: './cards/4_of_hearts.svg'}, { value: 5, img: './cards/5_of_hearts.svg'}, { value: 6, img: './cards/6_of_hearts.svg'}, { value: 7, img: './cards/7_of_hearts.svg'}, { value: 8, img: './cards/8_of_hearts.svg'}, { value: 9, img: './cards/9_of_hearts.svg'}, { value: 10, img: './cards/10_of_hearts.svg'}, { value: 10, img: './cards/jack_of_hearts2.svg'}, { value: 10, img: './cards/queen_of_hearts2.svg'}, { value: 10, img: './cards/king_of_hearts2.svg'}, { value: 1, img: './cards/ace_of_diamonds.svg'}, { value: 2, img: './cards/2_of_diamonds.svg'}, { value: 3, img: './cards/3_of_diamonds.svg'}, { value: 4, img: './cards/4_of_diamonds.svg'}, { value: 5, img: './cards/5_of_diamonds.svg'}, { value: 6, img: './cards/6_of_diamonds.svg'}, { value: 7, img: './cards/7_of_diamonds.svg'}, { value: 8, img: './cards/8_of_diamonds.svg'}, { value: 9, img: './cards/9_of_diamonds.svg'}, { value: 10, img: './cards/10_of_diamonds.svg'}, { value: 10, img: './cards/jack_of_diamonds2.svg'}, { value: 10, img: './cards/queen_of_diamonds2.svg'}, { value: 10, img: './cards/king_of_diamonds2.svg'}, ];
Let’s use the game assets
📝 Task 8: The Card Component
- Create a new file in
src/
called:Card.jsx
- (the capital ‘C’ matters)
- Insert the following code into
Card.jsx
// props is how we pass data to components function Card(props) { // this component just returns an <img /> return ( <img className='card' src={props.card.img} alt='' /> ); } export default Card
- Import Card into App.jsx
import Card from './Card';
- Test the Card Component in
App.jsx
- add a
<div className="hand">...</div>
- inside the
<div>
create a Card component with<Card />
- pass data to the component with
<Card card={...} />
Like this:
return ( <div className="App"> <h1>Card Game</h1> <p>Score: {score}</p> <button onClick={increaseScore} >Add Score</button> <div className="hand"> <Card card={cardData[0] }/> <Card card={cardData[13] }/> <Card card={cardData[26] }/> <Card card={cardData[39] }/> </div> {score === 21 && <h1>You won!</h1>} {score > 21 && <h1>You lost</h1>} </div> );
Add a state variable for the player’s cards. An array of numbers, where each number corresponds to an index in cardData
.
const [hand, setHand] = React.useState([0, 13, 26, 39]);
JavaScript’s Array.map()
is a function which transforms one array into another array. In JSX, it is commonplace to use Array.map()
to create arrays of components. Below, we are transforming our array of numbers into an array of <Card />
components.
Add the code below into your App.jsx
(replace the div
that you just added with the following):
<div className="hand">
{hand.map((c) => <Card key={c} card={cardData[c]}/>)}
</div>
Lets add a function to add a random card to the player’s hand, and increase their score
Since our hand
array is a state variable we can’t edit it directly. So, we first copy the array using the spread operator; modify the copy; then set the state variable to the modified copy.
function addNewCard() {
// random card
const newCard = Math.floor(Math.random() * cardData.length);
// update player's hand
const handCopy = [...hand]; // spread operator: deep copy array
handCopy.push(newCard);
setHand(handCopy);
// update score
setScore(score + cardData[newCard].value);
}
Add a button to call our addNewCard()
function. The button auto-disables when the player wins/loses.
<button onClick={addNewCard} disabled={score >= 21} >New Card</button>
This button replaces the Add Score button, so remove the Add Score button.
We don’t need the four Aces that we added earlier, so set the hand
state to an empty array. The updated version looks like:
const [hand, setHand] = React.useState([]);
Add a function to reset the game
function clearHand() {
setHand([]);
setScore(0);
}
Add a button to reset the game
<button onClick={clearHand} >New Game</button>
That’s it.
The game is ready to play, and hopefully you learned some React syntax along the way!
If you’re familar with Black Jack you know we simplified the game.
If you have time you could easily implement the rest of Black Jack.
- Deal two random cards at the start of the game
- Track the cards which have been dealt (don’t deal duplicate cards)
- If player is dealt an Ace, they should choose
1
or11
- you could use conditional rendering to display buttons for the choice
- Play against the dealer
- the dealer will need their own hand
- the player’s choices should be:
- ‘Hit’ for a new card
- ‘Stand’ to keep their hand
- After the player Stands
- the dealer adds new cards until they win, or bust
- Implement a betting system?
This lab is worth marks. be sure to get signed off.