Soft Open 004
Hello to everyone!
If you’re new here then here is an update of what is happening:
The Soft Open series is a record of me learning software engineering
I’m learning software engineering because:
I have an idea for something really cool
I find it really challenging
I’m learning "in the open" because
Mistakes are easier to spot and fix
Mistakes are good, they mean learning happens faster.
The really cool idea is:
Scratch for Data-Viz
I have an idea about how this app will work, and I am building towards that. All of the learning I am currently doing is towards that end. Unless I am Yak Shaving. I have a couple of libraries that I want to use, and they are both made in React. So I’m learning about it as a possible solution to the problem!
Tic Tac Toe in React
This is a record of me working through this tutorial. It contains a lot of technical stuff. And it is long. Just a heads up to say that anyone who manages to read and understand all of this deserves some kind of gold star. At the end is a bonus section where I expanded the game past the tutorial: so read on for that!
I thought about doing recording everything I did as I went through, but the tutorial is pretty well written. If you’re interested in learning React I would definitely work through it. Here I’m just highlighting anything that I didn’t understand while I went through the tutorial and anything that I had to look up.
Using the Bot
Although I do use chatGPT to give me answers, I write up everything in my own words - especially the code. There’s a certain amount of muscle memory that comes in from coding. It’s much easier for me to grok things if I write the code out. The bot is a useful tool, but in its current form it works best as a co-pilot. For example I can ask it to clarify or expand, then I read all of the output and synthesise it in a way that makes sense to me.
The tutorial:
I looked through the code blocks as provided initially to see what I wasn’t familiar with:
In App.js
:
export default function Square() {
return <button className="square">X</button>;
}
The default
keyword is used to tell other files using your code that this is the main function.
In index.js
:
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./styles.css";
import App from "./App";
import { StrictMode } from 'react';
This imports strictmode component from React.
Can also be done like this:
import React, { StrictMode } from "react";
This is a way of importing React and StrictMode
at the same time.
When curly braces are used around the modules it means you can use them without explicitly stating they are from their library. So instead of React.StrictMode
you can just say StrictMode
. This means you get nicer looking code.
What is StrictMode
?
It’s a tool for the development side of making a React app. It adds warnings and checks to help find issues. Once the code is in production, StrictMode
should be taken out, as it can slow the code down.StrictMode
will tell you
about problems in your code
whether there are side-effects during
render
if there are any deprecated features or APIs
If there are any unsafe lifecycle methods
if there are issues with functional components, such as unneeded re-renders or prop updates that could cause problems
about key warnings; if you render elements without
key
props, it will warn you.
Using StrictMode
if we are importingStrictMode
as above, then you can simply wrap a block in the tag as so:
<StrictMode>
//…
</StrictMode>
import React from 'react';
//…
<React.StrictMode>
//…
</React.StrictMode>
import { createRoot } from "react-dom/client";
This imports the createRoot
function from the react-dom/client
module.
What is createRoot
?
createRoot
is a fancy new way of doing the rendering since R18. It means that rendering can be done concurrently. It means the work of rendering can be broken into smaller pieces. It should also mean that the UI is more stable, so that one broken piece will not crash the whole system.
React Applications traditionally put App
into a root
element. That means that everything App renders will be put into that element.
In this code, we are getting the root
element from the DOM using good ole fashioned Web APIs, then making a new const
(also called root
), and immediately calling createRoot
upon it. Then running render
in StrictMode
inside.
const root = createRoot(document.getElementById("root"));
root.render(
<StrictMode>
<App />
</StrictMode>
);
What does render mean?
We keep talking about render
. So I’d like to take an aside to talk about what it means. render
defines what a component should display in the UI. render
returns a React element.
There are two kinds of React components: Class and Functional.
The render
function is used in the class components, rather than the functional components. This is because with functional components the output of the component serves the same purpose as render
.render
is a "pure function".
Pure Functions
Pure functions are an important part of the principles of React.
They:
are deterministic, therefore given the same inputs they will return the same results. That means nothing random, no external dependencies, and no hidden states.
Don’t create side effects. This means that it doesn’t modify any external state or data outside of the scope. It doesn’t change global variables, or make network requests or mutate data structures, or update the DOM.
Operates only with what it is given and returns the new value.
CSS code
In the CSS there was some interesting code about the grid structure of the rows:
.board-row:after {
clear: both;
content: '';
display: table;
}
This works on elements set after
the content of board-row
.clear: both
means that this pseudo element clears any floated elements (elements that use float:left
or float:right
) that come before it. The purpose of this is to make sure that elements that come after it do not float beside it.display:table
means that the elements behave like <table>
elements
The useState
hook returns an array with two elements.
We are initialising the value
constant using array destructuring:
const array = [1, 2, 3];
const [a, b, c] = array;
When useState
is called with null
inside it like this it sets the value of value
to null
. At the same time it creates a function to update value
called setValue
. Even though we can’t see the function in the Square
, it is there whenever we need it!
Thus we have a way to access the value
and update it.
If we wanted to increment the value, for example, we could create a function and put it inside the square like this:
const increment = () =>
{
setValue(value+1);
}
In this way, we can make Getters and Setters quickly and easily using React.
A Higher State of Consciousness
In order to manage the state
value of sibling elements we need to go up to the parent component. Here we need to determine game conditions so we need to see the state
of the Board
rather than the components that make it up. It’s better practice to keep the relevant props in the Board
then pass them down to the Square
s rather than store them in the Square
and have the Board
ask for them, and have them be passed up.
const [squares, setSquares] = useState(Array(9).fill(null));
Here we are using destructuring again, but instead we are setting a const
called squares
and filling it with an empty array that only contains 9 null
values. Eventually we will fill this up with spicy new values like X
or even O
.
Then we update the Board
code so that it passes the value down to the Square
:
<Square value={squares[0]} />
//…
<Square value={squares[9]} />
Since the Board
now maintains the value
of the Square
s, we need to make a way for the Square
to update the Board
state. The problem is that the state
of a component is private to that component.
So we have to pass a function down from Board
to Square
and have the Square
call that function when a Square
is clicked.
We do this by adding the function to the props of the Square
:
function Square({ value, onSquareClick })
{
return
(
<button
className="square"
onClick={onSquareClick}
>{value}
</button>
);
}
We write a new handleClick
to live inside Board
:
function handleClick(i)
{
const nextSquares = squares.slice();
nextSquares[i] = "X";
setSquares(nextSquares)
}
What does slice
do?
Normally when you use slice you use it with one or two arguments to pull out a part of the array.
When you use it with no arguments it makes a shallow copy of the whole array. This means that changes made to the copy will have no effect on the original. React encourages immutable data principles. This means when you change data, you don’t change the original, but instead swap it for a different reference. This makes it easier for you and React to track when changes happen, as it’s a matter of seeing if it’s the same object or not. It makes the rendering more predictable for React. It is related to PureComponents, but this is out of scope.
Then when we declare the Square
s inside the Board
we can pass in the function like this:
<Square value={squares[0]} onSquareClick={handleClick} />
…I then went on to finish the board…
Further work
import {useState} from 'react';
function Square({value, onSquareClick}) {
return <button className="square"
onClick={onSquareClick}>
{value} </button>;
}
export default function Board({n=6}) {
const [xIsNext, setXIsNext] = useState(true);
// make an empty n*n array
const [squares, setSquares] = useState(Array(n*n).fill(null));
//this gets called by the board when it renders
const renderSquare = (i) => {
return (
<Square
key={i}
value={squares[i]}
onSquareClick={ () => handleClick(i)}
/>
);
}
const rows = [];
for (let row = 0; row < n; row++) {
const row_squares = [];
for (let col = 0; col < n; col++) {
row_squares.push(renderSquare(row * n + col));
}
rows.push(
<div key={row} className="board-row">
{row_squares}
</div>
);
}
function handleClick(i) {
///is there a winner? if so, return early
if (squares[i] || calculateWinner(squares)) {
return;
}
const nextSquares = squares.slice();//shallow copy of the array
if (xIsNext) {
nextSquares[i] = "X";
} else {
nextSquares[i] = "O";
}
setSquares(nextSquares);//swap the arrays
setXIsNext(!xIsNext);//swap players
}
function calculateWinner(squares) {
const n = Math.sqrt(squares.length); // Calculate the size of the board
for (let i = 0; i < n; i++) { // Check rows
const row = squares.slice(i * n, (i + 1) * n);
if (row.every((value) => value === row[0] && value)) {
return row[0];
}
// Check columns
const col = [];
for (let j = 0; j < n; j++) {
col.push(squares[j * n + i]);
}
if (col.every((value) => value === col[0] && value)) {
return col[0];
}
}
// Check diagonals
const diagonal1 = [];
const diagonal2 = [];
for (let i = 0; i < n; i++) {
diagonal1.push(squares[i * (n + 1)]);
diagonal2.push(squares[(i + 1) * (n - 1)]);
}
if (diagonal1.every((value) => value === diagonal1[0] && value)) {
return diagonal1[0];
}
if (diagonal2.every((value) => value === diagonal2[0] && value)) {
return diagonal2[0];
}
return null; // No winner
}
const winner = calculateWinner(squares);
let status;
if (winner) {
status = "Winner: " + winner;
} else {
status = "Next player: " + (
xIsNext ? "X" : "O"
);
}
return (<>
<div className="status">
{status}</div>
<div>{rows}</div>
</>)
}
I checked this code on the 6x6 board and it seems to work ok. I made sure I read through everything to understand what it is doing. No doubt there are better, more elegant ways to do this, but it works and that is enough for me right now.
Phew, that was a long one.
Until next time!