Skip to content

kirillmarkelov/react-tic-tac-toe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tic-tac-toe-preview

React tic-tac-toe exerecises

This is my take on tasks provided at the end of React tic-tac-toe tutorial.

My why behind this repo

Completing these tasks took me about two weeks on and off as I was struggling with understanding many core concepts of React. Now, I decide to share my solution in case someone finds it difficult to comprehend things, as I did.

Disclaimer: source code may contain comments with innacurate understanding/interpretation of things, but I decided to leave it as is.

1. Display the location for each move in the format (col, row) in the move history list.

Disclaimer: before attempting this exercise, you need to complete 3rd exercise first. The reason is I used indexes from the loop to locate squares. If you find it difficult to grasp some of the stuff, refer to 3rd exercise.

The move history list is in Game component's render() method, and to display each move's location we need indexes of squares from Board component's render() method. So we'll pass the data from the child to the parent. We can't do this directly, so we'll use callback props.

  1. In the parent component, Game, add new state key to the constructor with an array literal as a value
this.state = {
  position = [],
}
  1. Below, create a function with two arguments to store props. Just like in handleClick(), create a copy of the array with .slice() array method, and add current step number as the second argument. Next, using spread syntax, pass the array you want to add to, and the value you adding to it. After that, update state with that variable
storePosition(col, row) {
  let position = this.state.position.slice(0, this.state.stepNumber);
    // creats a copy of the array; same way as in handleClick()

  position = [...position, [col, row]];
    // first argument is the array we want to add to; second is the value we want to add

  this.setState({
    position: position, // sets state with the variable with the same name
  });
}
  1. In <Board />, pass that function to onClick prop and add its arguments to the arrow function's arguments, so we can use it down in the component
<Board
  // ...
  onClick={(i, col, row) => {
    this.handleClick(i);
    this.storePosition(col, row);
  }}
  // ...
/>
  1. In Board's render() method, the child, add two arguments, col and row, to renderSquare() function (you can name them anyting you like, but they should match the second arguments in both .map() methods)
<div>
  {colsAndRows.map((square, col) =>
    <div className="board-row" key={col}>
      {colsAndRows.map((square, row) =>
        this.renderSquare((col * colsAndRows.length + row), col, row)
      )}
    </div>
  )}
</div>
  1. In renderSquare() function, add the same two arguments to it and to <Square /> onClick prop. We add + 1 to each argument, so 0 indexed square return 1 in the column and 1 in the row
renderSquare(i, col, row) { 
  return ( 
    <Square
      // ...
      onClick={() => this.props.onClick(i, col + 1, row + 1)}
      //...
    />
  );
}

Now, when you click on a square, its position will be passed to Game component.

  1. In Game's render() method, store our state in a variable and slightly modify desc varibale to display position of a square
const position = this.state.position;

let moves = history.map((step, move) => {
 const desc = move ?
  `Go to move #${move} (${position[move - 1]})`: // -1 because first item in move array occupied by 'go to game start' button
  'Go to game start';
}

2. Bold the currently selected item in the move list.

Just a CSS rule to color button on focus

button:focus {
  border: 2px solid black;
}

3. Rewrite Board to use two loops to make the squares instead of hardcoding them.

Previously, we generated text for move buttons using .map() method. You can read about it in React's documentation

  1. In Board's render() method, write this code. Some things to note:
  • An array variable serve us as a base for iteration using .map() method;
  • In .map() method, the first parameter is the content of the array's item; the second is an index of the item it is iterating on;
  • renderSquare()'s argument will be a key we will assign to Square component.
render() {
  const colsAndRows = [1, 2, 3];

  return (
    <div>
      {colsAndRows.map((square, col) =>
        <div className="board-row" key={col}> {/* there must be a key because it's rendered three times */}
          {colsAndRows.map((square, row) =>
            this.renderSquare(col * colsAndRows.length + row) // value will be from 0 to 8
          )}
        </div>
      )}
    </div>
  );
}
  1. In renderSquare() function, pass its argument as key value
renderSquare(i) {
  return ( 
    <Square
      value={this.props.squares[i]}
      onClick={() => this.props.onClick(i)}
      key={i} // (col * colsAndRows.length + row) for each element
    />
  );
}

4. Add a toggle button that lets you sort the moves in either ascending or descending order.

  1. In Game component's constructor, add new state
this.state = {
  reversed: false,
};
  1. In render() method's return statement, add button inside <div>
<div className="game-info">
  // ...
  <ol reversed={this.state.reversed}>{moves}</ol>
  <button onClick={() => {
    this.setState({ reversed: !this.state.reversed, }); // exclamation point flips boolean value
  }}>
    Reverse the list
  </button>
</div>

There are

  • New button with an event handler that flips current state on click.
  • Reversed html boolean attribute that changes order of numbers. Without it, button click will only change the order of array's items, thus order of buttons, not the accompanying numbers.
  1. In render() method, change moves variable from const to let so we can mutate it later. Next, add an if statement that will reverse the array on each re-render if truthy
let moves = history.map((step, move) => {
  // ...
}

if (this.state.reversed) {
  moves = moves.reverse(); // .reverse() is an array method 
}

5. When someone wins, highlight the three squares that caused the win.

To find out who wins, we use calculateWinner() function. To higlight squares, we can apply class to an element and color it with CSS; we can do this in Square. We could've called calculateWinner() directly in Square, but we couldn't pass Game's state with it. Instead, we pass this function from Game all the way to Square as a prop

  1. We need to slightly modify calculateWinner() to return not only a winning value but a winning combination as well.
if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
  return [squares[a], lines[i]];
}

Because one of the return statements is an array now, we should wrap the last one return statement in brackets too.

  1. In Game's render() method, add a new variable at the top to store the second return value and add index position to all instances of calculateWinner() function calls: in handleClick() and render()
const winner = calculateWinner(current.squares)[0];
const winningCombo = calculateWinner(current.squares)[1];
if (calculateWinner(squares)[0] || squares[i]) {
  return;
}
  1. In Game's return() statement, pass winningCombo variable to <Board /> component as a prop
<Board
  // ...
  winningCombo={winningCombo}
/>
  1. In Board's render() method, store the prop in a variable and pass it to renderSquare() as an argument
const winningCombo = this.props.winningCombo;

return (
  <div>
    {colsAndRows.map((square, col) =>
      <div className="board-row" key={col}>
        {colsAndRows.map((square, row) =>
          this.renderSquare( (col * colsAndRows.length + row), col, row, winningCombo )
        )}
      </div>
    )}
  </div>
);

Repeat the process and pass it as a prop to the <Square /> in renderSquare(). In addition, we need to pass another prop with the same value as in key prop

renderSquare(i, col, row, WinningCombo) {
  return ( 
    <Square
      // ...
      key={i}
      index={i}
      winningCombo={WinningCombo}
    />
  );
}
  1. Now, in Square
  • store props in variables;
  • initialize a new variable with an empty string to store class name in the future;
  • create an if statement to test if winning combination is initialized, and if it includes component's index. If so, initialize the variable with class name;
const winningCombo = props.winningCombo;
const index = props.index;
let comboClass = "";

if (winningCombo && winningCombo.includes(index)) {
  comboClass = 'combo';
}
  1. Write a CSS rule
.square.combo {
  background-color: green;
}
  1. Pass the variable to className
return (
  <button
    className={`square ${comboClass}`}
    // ...
  >
    {props.value}
  </button>
);

6. When no one wins, display a message about the result being a draw.

  1. In calcualateWinner(), add another if statement before last return statement to check if the array doesn't include any null
if (!squares.includes(null)) {
  return ['Draw'];
}
  1. In Game's render(), update an if statement with a winner text
if (winner === 'Draw') {
  status = winner;
} else if (winner) {
  status = 'Winner: ' + winner;
} else {
  status = 'Next player: ' + (this.state.xIsNext ? 'X' : 'O');
}

About

My take on tasks provided in React tic-tac-toe tutorial.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published