Skip to content
nomnivore edited this page Nov 8, 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 declarative JavaScript library for creating user interfaces. It is used in many popular large scale websites such as Facebook, Instagram, AirBnB, Netflix, and many more. React is primarily made up of three main parts that we will cover in this module: Components that can be separated and organized by their purpose and reused, hooks that allow you to persist state between renders and perform actions at specific parts of the components lifecycle (more below), and JSX that allows you to use an HTML-like syntax alongside JavaScript to create a webpage.

Some benefits of using React include:

  • It's flexible. React is an unopinionated framework, which means developers have the freedom to structure their applications in whatever way they prefer. This allows both small-scale and large-scale applications to be easy to read, refactor, and extend.
  • It's powerful. React handles a lot of tedious actions under the hood, such as updating the DOM in response to changes, meaning you can write less boilerplate code and more business code.
  • It's fast. React has optimized the operation of updating the DOM (the HTML content of a page) by only changing the content that is necessary. It does this all for you, which improves the developer experience. It also has a small file-size footprint compared to its competitors, making the initial page load faster even on slower connections.
  • It has great support. React was built and is maintained by a team at Facebook and the open-source community. It is used in thousands of major websites worldwide, has millions of downloads per week, and is one of the top repositories over on GitHub. It's popularity and vast use means it has great support for any issues that arise, tons of add-ons developed to solve common problems, and much more.

React applications are often called Single Page Applications (SPAs) because instead of splitting your website into many HTML pages, you declare a set of React components and let react handle which elements are being shown on the page based on the application's state and user activity (such as clicking a button).

Before learning React, you should be familiar with the fundamentals of both HTML and JavaScript, including newer features such as arrow functions and destructuring assignment, as these are concepts that are widely used in the world of React and will be utilized but not taught in this module.

Starting a React Project with Vite

The easiest way to start a React project with Vite is to use the most recent version of Visual Studio Code (VSC). Vite is something that you should be able to use as long as you have Node version 16+ installed. If you open the terminal within VSC ( from the menu at the top or by pressing CTL + SHIFT + ` ), and type in node -v you can see what version you have installed. If you have the correct version of Node installed, you should now be able to create a React application using the following steps:

  1. Open a Folder within the explorer. The next step will account for whether or not your folder already has things in it
  2. Type npm create vite folderName in the terminal. The folder name can be replaced with a . to use the current folder, but this will delete everything in the folder if you type y for the next prompt
  3. From the next two lists, use your arrow keys to select react, then javascript
  4. If you used npm create vite folderName in the previous step then you will need to enter the newly created folder using cd folderName
  5. Type npm install in the terminal to have npm install all of the React requirements. This step should also be done for React applications that you download since you need to locally install these requirements.
  6. Type npm run dev in the terminal. You can go to the page being run by either copy/pasting the link VSC provides into a browser, or CTRL + Clicking the link.

Congratulations on starting a React application! Now you can start customizing your application to suit your needs. If you only want to use the App.css file for visual customizations, remember to delete references to the other one from index.html.

Using JSX to Interop JS and HTML

If you tried creating a React application in the previous step you may have noticed that the index.html code does not contain many elements. In fact, the only major things it includes are things that mess with the title, and a script tag to a JSX file. However, if you look at the page running it has a functioning button and some pictures. This is all accomplished within the JSX code.

The JSX file accomplishes this by returning code that is very similar to HTML, but due to it being within a JavaScript file, it is easier to add functionality to your page. For example, the default page that is created has a button with a number that goes up with each click. While JavaScript usually requires several parts to make this button work, JSX only requires two parts. First is a useState hook that will be discussed later, and second is the button that utilizes this hook like so <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>.

Another significant difference between normal HTML and JSX is the fact that in order to use classes in HTML elements to direct css, you must use className=”name” instead of class=”name”. This is due to the fact that classes are a type of React component, so JSX needs to use className for HTML classes to allow for both to exist in the same file without conflict.

React Components

Components in React are a way of splitting your webpage into individual, reusable pieces. Essentially, a component is a JavaScript function that returns JSX and can optionally accept properties as arguments.

Here is an example of a simple function component:

function ColorDisplay({ color }) {
  return <p>My favorite color is {color}.</p>
}

Note that this function is using object destructuring syntax to read the component's properties. This is a React convention and is equivalent to the following:

function ColorDisplay(props) {
  return <p>My favorite color is {props.color}.</p>

We can now use this component as a child of other components with the following JSX syntax:

<ColorDisplay color="purple" />

Another recommended React convention is to place each component in its own unique file. If you do this, be sure to export the function and import it in any other file you wish to use it in. If you aren't sure how to do this, refer to the article on ES6 Modules at the top of this training module or explore the code used in the Lab below for an example.

If you take a look at the default app structure provided by Vite using npm create, you might notice that App is, itself, a component. This is crucial to understand because components are the fundamental building blocks of a React interface, not a lesser used extension of its capabilities.

Let's look at how we might add our ColorDisplay element to our App.

// ColorDisplay.jsx

// using arrow function syntax here
export const ColorDisplay = ({ color }) => {
  return <p>My favorite color is {color}.</p>
}
// App.jsx

import { ColorDisplay } from "./ColorDisplay"

function App() {
  return (
    <div>
      <h1>My favorites</h1>
      <ColorDisplay color="purple" />
    </div>
  )
}

Intro to React Hooks

Hooks are something that React added to make it easier to add and update information throughout the project. If you created an application in an earlier section, then you may have noticed that instead of the code for the button needing to reference this a lot (i.e. <button onClick={() => this.setState({ count: this.state.count + 1 })}>), it uses something called setCount similar to this <button onClick={() => setCount(count + 1) }>. You may also notice that the process of creating count is far shorter than usual. This is due to the useState react hook, which will be explained in more detail, along with two other common React Hooks, useEffect and useRef. If you are having trouble using one of these hooks, make sure you have imported all of them at the top of the document. If all three are being used the import line should look similar to the line below.

import { useState, useEffect, useRef } from 'react'

useState

useState is a hook that allows variables to stick around even after the App function rerenders. It does this by declaring a state variable that acts in the same way as this.state, meaning the application will continue to hold onto the variable after the function is exited. useState requires only one argument, the initial value of the variable. This initial value can be anything you like, as long as it is the same data type as what you want the variable to be. For example, in the lab at the end of the module you will see that you can use the useState variable to create a boolean with a default true state or an array filled with null.

If you created an application in the Vite section, you may have noticed that the full line of code for creating a variable with useState is const [count, setCount] = useState(0). While we have spent some time evaluating the right half of this declaration, the left half is equally important to understand, especially the setCount portion, which you should recognize from the introduction. In this example, count is the variable that receives the default value, while setCount is a function that can be called later to change this value. This simplifies the process of updating the variable quite a bit because instead of having to do this.setState({ count: this.state.count + 1 }) to increase the variable you only need to call the function like this setCount(count + 1)

useEffect

useEffect hooks are something that run every time the application finishes rendering. This is useful for situations where you want the application to figure something out after everything else has been updated. For example, if you wanted to update the basic application that Vite creates to change the title every 10 clicks, you could add something like this.

useEffect(() => {
   if (count % 10 == 0 && count != 0){
     document.title = “Over ${count} clicks”;
   }
 });

You can have multiple useEffects in the same file, which is useful for situations where you want certain things to occur only when a specific variable is changed. For example, if the default Vite application had two buttons, one for count1 and one for count2, the previous code could be updated to make the document title only use count1’s total, and as a result you only need this useEffect to run when button1 is clicked. To do that a useEffect similar to the one below could be used.

useEffect(() => {
   if (count1 % 10 == 0 && count1 != 0){
     document.title = “Over ${count1} clicks”;
   }
 }, [count1]);

Because [count1] is specified at the end, it makes it possible for me to add a useEffect before it, maybe one that uses both counts for the title, with the count1 useEffect being ignored if someone keeps clicking button2.

useRef

useRef is somewhat similar to useState, with one of the largest differences being the fact that using the variable created with useRef will not cause a rerender. This is important because if your code includes useEffect, there may be times that you want to update something on the page without having it trigger. You can use useRef by creating a variable like this const importantMsg = useRef(“Insert important text here”);, then you could reference it in your JSX like this <span ref={importantMsg}></span> to put the message on your page. Alternatively, you could also display the content by using <span>{importantMsg.current}</span>

You may have noticed that the left half of the variable declaration did not include a setImportantMsg. That is because instead of using a function to update the contents, you use .current, so updating importantMsg instead looks like importantMsg.current.innerText = "MESSAGE!"; if your JSX used ref=, or importantMsg.current = "message"; if you displayed it using {importantMsg.current}.

Component Lifecycle

To help tie everything together, let's summarize the cycle that a component goes through. It is important to understand how everything happens under the hood to know why and where React hooks are important. We'll discuss this further later in this section.

You can think of a React component like a program on your computer. First, you install the program (initialization & mounting). You are notified later that it has updates available (state has changed), so you install updates (updating). When you are done with the program, you uninstall it from your machine (unmounting) which performs various cleanup actions such as clearing cache or deleting temporary files.

Here is what that process looks like with a React component:

Initialization

  • The component is instantiated along with any declared variables (like useState and useRef hooks or otherwise).
  • This is done only once, on page load.

Mounting (first render)

  • Component is rendered on the page (the return of a functional component)
  • useEffect hooks are called, where dependencies allow. If you only want an effect to run on the first render, you would use useEffect(myFunc, []) where myFunc is any function.

Updating (subsequent renders)

  • Happens when the component's state is updated (with the setter function provided by useState)
  • The component is rendered again, updating the page's HTML to show changes
  • useEffect hooks are called, where dependencies allow.

Unmounting

  • When a component is removed from the page, it is "unmounted."
  • Cleanup functions returned by your useEffect hooks are ran. This is useful for doing things such as aborting pending API calls and more.

By knowing this order of operations, it becomes clear that useEffect and useState, in particular, are very powerful hooks. useState allows us to declare which variables should change how our application looks, and useEffect allows us to perform certain actions after a page loads or renders, such as calls to an external API, and perform any necessary cleanup operations that may be warranted as a result.

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>

If you want to make sure that everything is working at this point, you can change {value} to {idx} to ensure it is working. If the squares have numbers in them, then everything is working, and you should switch the idx back to value.

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 using ...oldSquares, 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 opening tag. 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 return JSX of App to look like this:

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

If you want to reset the board at this point, you will need to refresh the page.

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 opening tag:

// 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

Beyond the Training Module

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

Clone this wiki locally