DEV Community

loading...
Cover image for Let's create a tic-tac-toe with React.

Let's create a tic-tac-toe with React.

Navraj Sandhu
17 years old React and Django developer. Learning new things and leveling up everyday.
・4 min read

Welcome Everybody,
In this article we are going to create a tic-tac-toe app using react and react hooks.

Before we start creating our app, You should familiar
with Javscript, React and React-hooks.

So, Without wasting any time let's move to project setup.

Project setup

In the terminal go to the directory where you want to create you react-app.
and then run the following command.

npx create-react-app tic-tac-toe 
Enter fullscreen mode Exit fullscreen mode

For creating our app I am using create-react-app framework.
If you wanna create it manualy you can do so.

You can delete App.test.js, setupTests.js and logo.svg.
After that clean App.js like following :


import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      Hello I am react app.
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Run yarn start or npm start for running dev server on localhost.

That's enough for project setup. Let's move to next one.

Let's build our app.

Breaking down our app into function.

  • First in src/App.js, Create a function called calculateWinner to get winner from an array.

function calculateWinner(squares) {
  const lines = [
    [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 (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

Enter fullscreen mode Exit fullscreen mode

We are storing pattern of lines in case of a winner in lines array and if we see any of these pattern at any time of the game we will declare winner.

  • For calculating next turn create a function called calculateNextValue.

function calculateNextValue(squares) {
  return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
}

Enter fullscreen mode Exit fullscreen mode
  • then, we can check the current status of game by creating next function.

function calculateStatus(winner, squares, nextValue) {
  return winner
    ? `Winner: ${winner}`
    : squares.every(Boolean)
    ? `Scratch: Cat's game`
    : `Next player: ${nextValue}`
}

Enter fullscreen mode Exit fullscreen mode

Now in App.css let's write some styles for our game-board


.game {
  font: 14px 'Century Gothic', Futura, sans-serif;
  margin: 20px;
  min-height: 260px;
}

.game ol,
.game ul {
  padding-left: 30px;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

.status {
  margin-bottom: 10px;
}

.restart {
  margin-top: 10px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
  background: #ddd;
}

.game {
  display: flex;
  flex-direction: row;
}

.game-info {
  margin-left: 20px;
  min-width: 190px;
}

Enter fullscreen mode Exit fullscreen mode

Now let's create our game-board in App.js

function Board() {
  const [squares, setSquares] = React.useState(Array(9).fill(null))

  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(winner, squares, nextValue)

  function selectSquare(square) {
    if (winner || squares[square]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square] = nextValue
    setSquares(squaresCopy)
  }

  function restart() {
    setSquares(Array(9).fill(null))
  }

  function renderSquare(i) {
    return (
      <button className="square" onClick={() => selectSquare(i)}>
        {squares[i]}
      </button>
    )
  }

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
      <button className="restart" onClick={restart}>
        restart
      </button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Now create a Game() function and put our Board() Component

inside it.


function Game() {
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode

then render Game() function inside App().


function App() {
  return <Game />
}
Enter fullscreen mode Exit fullscreen mode

At the end our App.js should look like this.


import React from 'react';
import "./App.css";

function Board() {
  const [squares, setSquares] = React.useState(Array(9).fill(null))

  const nextValue = calculateNextValue(squares)
  const winner = calculateWinner(squares)
  const status = calculateStatus(winner, squares, nextValue)

  function selectSquare(square) {
    if (winner || squares[square]) {
      return
    }
    const squaresCopy = [...squares]
    squaresCopy[square] = nextValue
    setSquares(squaresCopy)
  }

  function restart() {
    setSquares(Array(9).fill(null))
  }

  function renderSquare(i) {
    return (
      <button className="square" onClick={() => selectSquare(i)}>
        {squares[i]}
      </button>
    )
  }

  return (
    <div>
      <div className="status">{status}</div>
      <div className="board-row">
        {renderSquare(0)}
        {renderSquare(1)}
        {renderSquare(2)}
      </div>
      <div className="board-row">
        {renderSquare(3)}
        {renderSquare(4)}
        {renderSquare(5)}
      </div>
      <div className="board-row">
        {renderSquare(6)}
        {renderSquare(7)}
        {renderSquare(8)}
      </div>
      <button className="restart" onClick={restart}>
        restart
      </button>
    </div>
  )
}

function Game() {
  return (
    <div className="game">
      <div className="game-board">
        <Board />
      </div>
    </div>
  )
}

function calculateStatus(winner, squares, nextValue) {
  return winner
    ? `Winner: ${winner}`
    : squares.every(Boolean)
    ? `Scratch: Cat's game`
    : `Next player: ${nextValue}`
}

function calculateNextValue(squares) {
  return squares.filter(Boolean).length % 2 === 0 ? 'X' : 'O'
}

function calculateWinner(squares) {
  const lines = [
    [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 (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i]
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a]
    }
  }
  return null
}

function App() {
  return <Game />
}

export default App
Enter fullscreen mode Exit fullscreen mode

And that is it. We are done with that.

πŸ’‘ Extras

You can save the game's squares to browser localhost by using the following hook:


function useLocalStorageState(
  key,
  defaultValue = '',
  {serialize = JSON.stringify, deserialize = JSON.parse} = {},
) {
  const [state, setState] = React.useState(() => {
    const valueInLocalStorage = window.localStorage.getItem(key)
    if (valueInLocalStorage) {
      return deserialize(valueInLocalStorage)
    }
    return typeof defaultValue === 'function' ? defaultValue() : defaultValue
  })

  const prevKeyRef = React.useRef(key)

  React.useEffect(() => {
    const prevKey = prevKeyRef.current
    if (prevKey !== key) {
      window.localStorage.removeItem(prevKey)
    }
    prevKeyRef.current = key
    window.localStorage.setItem(key, serialize(state))
  }, [key, state, serialize])

  return [state, setState]
}

export {useLocalStorageState}
Enter fullscreen mode Exit fullscreen mode

Thanks for reading.

Happy coding 😍.

Discussion (7)

Collapse
lukeshiru profile image
LUKESHIRU

Nice one! A few things I suggest or would do different:

  • Instead of having divs for every row, and using renderSquare, I would just map over squares and use CSS to show them as rows (with flex).
  • I would move the state to the top App, and have the Board be a stateless component receiving props and emitting events.
  • I would avoid null and just use undefined.
  • calculateWinner can be simplified drastically.
  • I would use CSS Modules instead of plain old CSS.
  • I would use more useCallback and useMemo to avoid unnecessary re-renders.
  • I would have the default state of the board in a constant so I don't have to create a new array every time the user restarts.
  • Ideally we should have every util in its own file, same for every component (I didn't did it in the example below, but is the ideal approach for bigger projects).

Here is a CodeSandbox with all the suggestions applied:

Cheers!

Collapse
asyncnavi profile image
Navraj Sandhu Author

Thanks for suggestions.πŸ’

Collapse
jselbie profile image
John Selbie • Edited

I built my own React-Tac-Toe game a few months ago as part of my own learning effort.

You can access the game and the source on my Github at github.com/jselbie/react-tac-toe

Collapse
asyncnavi profile image
Navraj Sandhu Author

Great work.

Collapse
matengodev profile image
Davis O Matengo

This was awesome

Collapse
asyncnavi profile image
Navraj Sandhu Author

πŸ™Thanks

Some comments have been hidden by the post's author - find out more