COSC203 Web, Databases, and Networks
Toggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

Lab 6: React

🎯 Lab Objective

In this lab, you will learn React

This lab teaches the following concepts:

  1. Template Strings
  2. The <template> element
  3. React Components and JSX
  4. React State

Table of Contents

1. JavaScript Template Strings

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

Backticks

Template strings are delineated with Backticks:`string`.

Backtick is on your keyboard (assuming you have a standard US QWERTY keyboard) below the Esc key.

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}`);
 Tip

Many 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 Strings

A simple To-Do List application

  1. Create a new folder for this project
  2. Copy the code below into a .html file
  3. Replace const time = ... with a Template String
  4. Replace item.innerHTML = with a Template String
  5. 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>


2. HTML Template Tag

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
  1. 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 the head section of the page below the script section.

<template id="itemTemplate">
    <div class="itemClass">
        <input type="checkbox" />
        <span class="textClass"></span>
        <span class="timeClass"></span>
    </div>
</template>
  1. 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
  1. Test the functionality

3. React (UI Framework)

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…

Introducing React

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.

A Card Game in React

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 App

React expects a certain structure for the project. To create the project we will use the built tool vite.

  1. 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
  2. Start the dev server: npm run dev
  3. Visit the hosted site http://localhost:5173/
  4. Open the project directory with VS Code

React Project Structure

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 into
  • public/ root folder that is served as a website
    • vite.svg – the Vite logo that is displayed on the default page
  • src/ the source code for the project
    • App.jsx a provided example component
    • App.css style for the above component (App.jsx)
    • index.css global styles for the entire project
    • main.jsx the initial script which inserts components into index.html
    • assets/react.svg asset which can be deleted

React Components (and JSX)

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
  1. Replace entire the contents of App.jsx with the one provided above

    • you may also delete vite.svg since it is no longer being used
  2. Set score to different number

    • save, reload, and check it updates
    • this isn’t 2-way data binding yet…

Component State

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
  1. Replace const score = 0; with:
const [score, setScore] = React.useState(0);
  1. 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);
}
  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>
  1. Test it
    • everytime you click the button the view updates!!!
    • Yeah! 2-Way Data Binding

Conditional Rendering

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
  1. 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>}
  1. 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>
    );
}
  1. Test the game out
    • get to the winning state
    • get to a losing state

Game Assets

We have provided some assets to make the game look nicer

📝 Task 7: Game Assets
  1. 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%;
}
  1. Download the card images here: cards.zip
    • unzip the images into the public/ folder
    • e.g. public/cards/ace_of_spades.svg'
  2. 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'},
];

The Card Component

Let’s use the game assets

📝 Task 8: The Card Component
  1. Create a new file in src/ called: Card.jsx
    • (the capital ‘C’ matters)
  2. 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
  1. Import Card into App.jsx
import Card from './Card';
  1. 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>
);


Hand State

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]);

Render The Player’s Hand

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>

Adding Cards

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([]);

Reset the Game

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!


4. Optional Extension: Real Black Jack

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.

  1. Deal two random cards at the start of the game
  2. Track the cards which have been dealt (don’t deal duplicate cards)
  3. If player is dealt an Ace, they should choose 1 or 11
    • you could use conditional rendering to display buttons for the choice
  4. 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
  5. Implement a betting system?

5. Marking Off

This lab is worth marks. be sure to get signed off.