Skip to content
Hosler12 edited this page Nov 4, 2022 · 30 revisions

Welcome to the React Beginner Training Module

This training module is designed to get you started with React by briefly covering some of the fundamental concepts and then demonstrate how to build a basic TicTacToe application with React. It will also introduce Vite, a front-end toolchain that makes developing with React quick and easy.

Requirements to get started

Training Module Overview

This training module is intended to help people new to React understand some of its basic concepts. This module will start by explaining why someone would be interested in using React, along with explaining one of the popular ways of starting a React project. After providing this introductory background, we will explain how you can use jsx to create a web page, explain what React components are and their lifecycle, and provide an introductory explanation of what React hooks are and how to use them.

Learning Objectives

  • Create a starter project with Vite
  • Use JSX format to dynamically render HTML
  • Create and use a function component
  • Understand the lifecycle of a component
  • Understand state management using React Hooks

Introduction to React (What is React)

React is a framework for JavaScript that makes it easy to create a webpage that users can interact with. React accomplishes this using three things, Components that can be called to accomplish tasks, Hooks that will allow information to be kept between renders, and JSX that allows you to pass html like code from your JavaScript to the page.

The main reason "good understanding of both HTML and JavaScript" is included in the list of requirements is the fact that while these may be new concepts to you, they will still utilize basic concepts like arrow functions and calling the class of a specific html element.

Starting a React Project with Vite

Using JSX to interop JS and HTML

React Components

Common React Hooks

useState

useEffect

useRef

Component Lifecycle

Lab

In this section of the training module we are going to develop a simple TicTacToe application that leverages many of the React concepts that were explained above.

Code snippets provided in this tutorial may contain comments // like this... indicating the location of surrounding code. These comments are purely to help provide context and do not need to be copied into your project.

Getting Started

Start off by downloading our starter files to your local machine, which contain a barebones React/Vite app containing a bit of the boilerplate code and CSS styles for the project.

Open the project in VSCode and open a terminal by clicking Terminal->New Terminal at the top of your screen.

Run the following command in your terminal to install the necessary dependencies:

npm install

If you get an error here, you most likely do not have Node.js properly installed on your machine, or it is out of date. You can find an installer link down at the bottom of the page, in the Additional Resources section.

Next, we'll start a live development server by running the following command so that we can see our changes locally whenever we save:

npm run dev

Creating the Board

The basic concept is to store our X and O placement data in an array and use React to convert that into the 3x3 board on the screen.

The first thing we will need to do is create a variable containing our board's current state. To do that, we will use the useState hook to allow this data to persist between renders.

Add the following line at the top of the App function component in App.jsx:

// function App() {

const [squares, setSquares] = useState(new Array(9).fill(null));

We are going to use a helper function within our App component to create a series of 9 Square components to represent this array. Take note of the Square.jsx file, which contains a nearly empty component, and how we have already imported it in App.jsx using destructuring syntax:

// App.jsx
import { Square } from "./Square";

Add the following function inside the App component:

// const [squares, ...

function renderSquares() {
  const arr = [];

  for (let i = 0; i < squares.length; i++) {
    arr.push(
      <Square
        key={i}
        idx={i}
        value={squares[i]}
      />
    );
  }

  return arr;
}

// return (...

Few things to note here. We are using JSX inside of the arr.push() method, showing that you can utilize JSX elements anywhere within your code. In particular, we have a <Square /> component and are passing a few parameters (properties) with its construction:

  • key: React requires this parameter whenever you have a bunch of components in an array-like structure. We can't access it directly.
  • idx: Because we can't access the key ourselves, we need to set an extra property to track the element's index in the array.
  • value: This will be what the Square renders inside of it on the screen.

Now, lets add this function inside of our App's return statement JSX, in the board div:

<div className="board">
  {renderSquares()}
</div>

Your app should now look something like this:

image

Next we need to add our properties to the Square.jsx and make them display their proper value.

Change the Square component declaration line to look like this:

// Square.jsx
export const Square = ({ idx, value }) => {

Replace the content inside the return statement div with the value property:

// return (
<div className="square">
  {value}
</div>

Checkpoint 1 Snapshot

Adding Click Functionality and Alternating Turns

We need another variable to determine which player's turn it is. Just like with our board squares, we are going to use another useState hook with a default value of true.

Add the following lines to the top of your App component:

// const [squares, ...
const [turnX, setTurnX] = useState(true)

We're going to make a function within App handle what happens whenever a square gets clicked, and then pass that function to our Square component.

Let's add the function to our App now:

function squareClicked(idx) {
  setSquares((oldSquares) => {
    const newSquares = [...oldSquares];
    newSquares[idx] = turnX ? "X" : "O";

    return newSquares;
  });

  setTurnX((current) => !current);
}

So what's going on here? We are using the setSquares function to update our squares state variable. Since we want to base the new value off of the old value, we pass in a function, copy the array, modify it based on who's turn it is, and return the new array to be the board's state. Next, we use setTurnX with the same function syntax (the return is implied here because it is one-line) to flip the boolean variable and change turns.

Pass the function as a property to our Square component inside of our renderSquares function by adding this line to our <Square /> JSX:

// arr.push(
  // <Square
  // ...
  handleClick={squareClicked}
// /> ...

In Square.jsx, we need to accept the new property and utilize it in a onClick HTML property in our div. We'll give the onClick property an inline function that only calls handleClick if the square is empty.

Apply these changes by updating the Square.jsx file like this:

// Square.jsx
export const Square = ({ idx, value, handleClick }) => {
  return (
    <div
      className="square"
      onClick={() => (value ? null : handleClick(idx))}
    >
      {value}
    </div>
  );
};

You should now be able to test the functionality of clicking on a square and watching the alternating 'X's and 'O's appear as you click. But how do we know who's turn it is? Let's add a display for that now.

Change the <h2> element in the App return JSX to look like this:

// App.jsx
// <h1 className="title"> ...
<h2>
  Player Turn:
  <span className="turnIndicator">{turnX ? "X" : "O"}</span>
</h2>

Checkpoint 2 Snapshot

Checking Game Over and Resets

We're going to add an element to our App JSX to display the game over message. This time, we'll take advantage of the useRef hook to store a reference to the message element. We're also going to use another useState hook to check if the game is over or not.

Let's add both of these hooks now to the top of our App component.

// App.jsx

// const [turnX, ...
const [gameOver, setGameOver] = useState(false);
const winMsg = useRef(null);

Add the "winMsg" span to our return JSX and use the ref= attribute to bind this element to our useRef variable declared in the last snippet.

// App.jsx

// </h2>

<span className="winMsg" ref={winMsg}></span>

// <div className="board"> ...

While we're at it, we can add a guard clause to our squareClicked function and prevent square clicks after a game ends.

// App.js

// function squareClicked(idx) {
  if (gameOver) return;

  // setSquares( ...

Here is the function that checks for a game winner or draw. It simply loops over the valid win conditions and checks if any of the trios match.

Add this inside the App component above the return statement:

// App.js

function checkWinner() {
  const conditions = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];

  for (const condition of conditions) {
    if (
      squares[condition[0] &&
      squares[condition[0]] == squares[condition[1]] &&
      squares[condition[1]] == squares[condition[2]]
    ) {
      return squares[condition[0]]; // returns "X" or "O"
    }
  }

  // check draw
  if (squares.some((val) => val === null)) {
    // there are turns left
    return false;
  } else {
    return "DRAW";
  }
}

// return ( ...

So we have the function that checks for winners, but where do we call it? That is where our first useEffect hook comes in. The function passed into the useEffect will run each time the component rendered, after rendering. Since clicking on an open square causes it to re-render, this is the perfect time to check if someone has won the game.

Let's add that hook now, in our App component just after the rest of our hook declarations.

// App.jsx

// const winMsg = ...

useEffect(() => {
  const winner = checkWinner();
  if (winner) {
    setGameOver(true);
    if (winner == "DRAW") {
      winMsg.current.innerText = "It's a draw!";
    } else {
      winMsg.current.innerText = winner + " wins!";
    }
  } else {
    winMsg.current.innerText = "";
  }
});

First, we check if there is a winner/draw. If not, simply clear winMsg text. If there is a winner, we need to set the gameOver state to true, and because this value is not dependent on previous state we can pass in the value directly instead of using a callback function. After that, the message is once again set to reflect the status of the game. Notice how we are using winMsg.current to access the current value of the reference winMsg.

At this point, we can click squares to take turns and the game will detect when the game is over, preventing us from clicking more squares. Now that we have all of our state variables, we can make the reset function and set the state variables back to their defaults.

Add this function to your App component, anywhere after the hooks and before the return statement:

// App.jsx

function resetGame() {
  setSquares(new Array(9).fill(null));
  setTurnX(true);
  setGameOver(false);
}

It's pretty simple, but it's a great demonstration of how powerful React can be. Just by resetting our state variables, our app is back to a clean slate. Let's wire our <button> up to this function using the onClick= attribute.

Change the <button> element to look like this, inside the App component's return statement:

// App.jsx

// <div className="board" ...

<button onClick={resetGame}>Reset</button>

Test your application and ensure that there are no errors. your app now has full functionality!

Checkpoint 3 Snapshot

Extra: Rainbow Hovers for Squares

Time to make things look a little crazy. Let's head over to our Square component and add a hover effect.

We are going to use a useRef hook to store a reference to the square's <div> element, and our random function from the util.js file.

Add the following imports to the top of Square.jsx:

// Square.jsx

import { useRef } from "react";
import { random } from "./util";

Declare a useRef variable at the top inside of the Square component.

// Square.jsx

// export const Square = ({ idx, ...
  const theSquare = useRef(null);

Connect our ref to the <div> in the return JSX by adding the ref= attribute:

// Square.jsx

// <div
// className="square"
// onClick={...
  ref={theSquare}
// >

Next, we're going to add a function to handle the mouseOver event and pass it as an attribute of the <div> in the return JSX.

Add this function to the Square component, after the useRef hook and before the return statement:

// Square.jsx

// const theSquare = ...

function handleMouseOver(event) {
  const newColor = `rgba(${random(255)}, ${random(255)}, ${random(
    255
  )}, .65)`;

  theSquare.current.style.background = newColor;
}

// return ( ...

Here we are using the random utility function to assist in generating a randomized RGBA color. The syntax may look a little funny if you aren't familiar with Template Literals, but it isn't important for this demonstration.

Add this function as an event attribute to our <div>:

// Square.jsx

// <div
// ...
  onMouseOver={handleMouseOver}
// >

Finally, we'll add an inline event handler to reset the background color when the mouse leaves the square.

Add this event handler as another attribute of the <div>:

// Square.jsx

// <div
// ...
  onMouseOut={() => (theSquare.current.style.background = "transparent")}
// >

Enjoy the lights!

Extra: Border Styles for New Games

The final extra add-on for this application is going to change the border style of the squares every time a new game starts. To do this, we're going to use another useEffect hook, but it will be declared a little differently than the first one. Remember that the useEffect hook can be passed an array of dependencies that will cause the callback function to re-fire.

Since we're utilizing that random helper function from before, we'll need to import that to our App.jsx as well.

Add the random import to the top of the App.jsx file:

// App.jsx

import { random } from "./util";

Add this hook to the App component just after the existing useEffect hook that we already have:

// App.jsx

// useEffect() => {
// ...
// });

useEffect(() => {
  if (!gameOver) {
    const allSquares = document.getElementsByClassName("square");
    const borders = [
      "hidden",
      "dotted",
      "dashed",
      "solid",
      "double",
      "groove",
      "ridge",
    ];

    const newBorder = borders[random(borders.length) - 1];
    for (const sq of allSquares) {
      sq.style.borderStyle = newBorder;
    }
  }
}, [gameOver]);

Note that this may not be best-practice for manipulating the DOM in React, but will work for our demonstration purposes here

So, what we are doing here is waiting for our gameOver dependency supplied in the second parameter of the hook to change, which happens when the app first launches (because of the initial useState declaration), when the game is over, and when the "RESET" button is clicked after the game is over. If, after any of these actions, gameOver == false, we pick a random border style from the supplied list and change the style of all of the squares to match. The most important thing to note here is how the dependency array ties in with the callback function, both supplied to the useEffect hook.

Your app should now have some fancy border styles and color highlights on hover!

image

Checkpoint 4 Snapshot

Bonus Challenge

If you'd like to practice further by applying React concepts on your own, try making further modifications and adding additional features to this application. You won't be provided the code for these and they may require additional research, but generous hints will be provided if needed. Good luck!

[ 1 ]: Unique Text Color for X and O

We want to display any Square containing an X with one color, and Squares containing O with another. For example, red and blue. Bonus points if the user can change the colors themselves.

See Hints
  • Square.jsx can read its own value property and use that to set a custom CSS class or React style object.
  • An HTML color input sets an inner value of a hex color string and fires the onChange event.
  • You may need to pass an additional properties to the Square component.

[ 2 ]: Scorekeeping

We want to keep a tally of how many games each player wins and display a simple scoreboard on screen. Bonus points if the scoreboard persists between refreshes (or closing/opening the browser) and includes functionality to reset.

See Hints
  • Think about React hooks and the best way to update a stateful value in this context
  • The localStorage API allows for simple key-value pair storage in the user's browser.
  • How can we make sure we are only loading from localStorage (or wherever the data persists) once, upon initial page load?
  • Reset functionality already exists in this application for other data and works very similarly. Take inspiration!

Assessment

Additional Resources

Node Installer

Clone this wiki locally