Shiva Bhusal
Shiva's Blog

Follow

Shiva's Blog

Follow

Tictac Toe using React Context API with Ruby On Rails

Shiva Bhusal's photo
Shiva Bhusal
·Jul 23, 2019·

7 min read

Play this article

Today we will see how to build a beautiful tic-tac-toe game. You can find the source code over here https://github.com/shivabhusal/react-context-api-tic-tac-toe

For Ruby On Rails Developers

  • Install Ruby
  • Install Latest Rails Gem.
rails new react-context-api-tic-tac-toe --webpack=react

This will generate a new Rails app in react-context-api-tic-tac-toe folder.

Why Rails?

We also need a back-end to authenticate User and store the game scores for that user. We will be using Devise to add authentication.

How do I setup React app?

From Rails 5 you can specify a webpack like react while generating app and all the necessary configurations are auto generated.

In this app too, in app/javascripts/pack/hello_react.jsx you can see a simple react app ready to run.

You just need to:-

  • Add generate a controller say homes with index action, because without a controller you wont be able to use the app.

      rails g controller homes index
    
  • In config/routes.rb add a line

      root 'homes#index'
    
  • In layouts/application.html.erb add line in head section

        <%= javascript_pack_tag 'hello_react', 'data-turbolinks-track': 'reload' %>
    
  • Then run rails server
        rails s
    
  • localhost:3000 will get you

      Homes#index
    
      Find me in app/views/homes/index.html.erb
    
      Hello React!
    
  • We will clear up the file views/homes/index.html.erb

React App Development

The UI of the app will gonna look similar to this. There might come some UI enhancements later on.

Tic Tac Toe

Building components

First of all we will remove some sample code from hello_react.jsx and rename it to tictactoe.jsx. Then create a folder app/javascripts/components to store all of our components.

We will be using bootstrap for building beautiful UI.

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">

Building the UI only

Files Tree

> tree app/javascript

app/javascript
├── channels
│   ├── consumer.js
│   └── index.js
├── components
│   ├── Canvas.jsx
│   ├── Square.jsx
│   ├── bootstrap
│   │   ├── Alert.jsx
│   │   └── NavBar.jsx
│   └── contexts.jsx
├── packs
│   ├── application.js
│   └── tictactoe.jsx
└── stylesheets
    └── styles.scss

5 directories, 10 files
// app/javascript/components/bootstrap/Alert.jsx
import React from 'react';

const AlertVarients = {
    notice: 'alert alert-primary',
    success: 'alert alert-success',
    error: 'alert alert-danger'
};

var Alert = (props) => (
    <div className={AlertVarients[props.type]} role="alert">
        {props.msg}
    </div>
);

export default Alert;
// app/javascript/components/bootstrap/NavBar.jsx

import React from 'react';

var NavBar = (props) => (
    <nav className="navbar navbar-expand-lg navbar-light bg-light">
        <button className="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarTogglerDemo03"
                aria-controls="navbarTogglerDemo03" aria-expanded="false" aria-label="Toggle navigation">
            <span className="navbar-toggler-icon"></span>
        </button>
        <a className="navbar-brand" href="#">Tic Tac Toe</a>

        <div className="collapse navbar-collapse" id="navbarTogglerDemo03">
            <ul className="navbar-nav mr-auto mt-2 mt-lg-0">
                <li className="nav-item active">
                    <a className="user nav-link" href="#">
                        <span className="name">{props.user.name}</span>

                        <span className="sr-only">(current)</span>
                        <span className="nav-avatar" style={{backgroundImage: `url(${props.user.avatar})`}} alt=""/>
                    </a>
                </li>
            </ul>

            <form className="form-inline my-2 my-lg-0">
                <input className="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search"/>
                <button className="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
            </form>

        </div>
    </nav>
);

export default NavBar;
// app/javascript/components/contexts.jsx
import React from 'react';
var SquareContext = React.createContext({});
export default SquareContext;
// app/javascript/components/Square.jsx
import React from 'react';
import SquareContext from './contexts'

// <!-- Since Squares are gonna have synamic value and events and gonna repeat -->
// <!-- We are going to create a separate component -->

export default function Square(props) {
    return (
        <SquareContext.Consumer>
            {
                ({state, handler}) => (
                    <div className="square" onClick={handler}>{state.squares[props.index]}</div>
                )

            }

        </SquareContext.Consumer>
    )
}
// app/javascript/components/Canvas.jsx
import React from 'react';
import Alert from './bootstrap/Alert';
import NavBar from './bootstrap/NavBar';
import Square from './Square';
import SquareContext from './contexts';


const DefaultCanvasState = {
    players: [
        {name: 'Emma', symbol: 'X', avatar: 'https://image.shutterstock.com/image-photo/beautiful-woman-face-closeup-beauty-260nw-1403676473.jpg'},
        {name: 'Lisa', symbol: 'O', avatar: 'https://static3.bigstockphoto.com/8/7/2/large1500/278719948.jpg'}],
    gameOver: true,
    nextTurn: 0,
    squares: Array(9).fill('X')
};


export default class Canvas extends React.Component {
    state = DefaultCanvasState;
    squareParam = {state: this.state, handler: this.playHandler};

    playHandler() {

    }

    render = () => (
        <React.Fragment>
            <NavBar user={this.state.players[0]}/>
            <Alert/>
            <div className="container p-5 text-center">
                <h6><span className="font-weight-bold">{this.state.players[this.state.nextTurn].name}</span> has next
                    turn to play!</h6>
                <div className="mt-4 row">
                    <div className="col-sm-4">
                        <h2>Player1</h2>
                        <h3>{this.state.players[0].name}</h3>
                        {this.drawTurnPen(0)}
                    </div>

                    <div className="col-sm-4">
                        <div className="canvas d-flex flex-wrap mx-auto">
                            <SquareContext.Provider value={this.squareParam}>
                                {this.genSquares()}
                            </SquareContext.Provider>
                            {this.drawPlayAgain()}
                        </div>
                    </div>

                    <div className="col-sm-4">
                        <h2>Player2</h2>
                        <h3>{this.state.players[1].name}</h3>
                        {this.drawTurnPen(1)}
                    </div>
                </div>
            </div>
        </React.Fragment>
    );


    drawTurnPen = (playerIndex = 0) => {
        if (this.state.nextTurn == playerIndex) {
            return (
                <i className="fas fa-pen-fancy fa-5x  text-success border rounded-circle"></i>
            )
        }
    };

    genSquares = () => (
        this.state.squares.map((_data, index) => (
            <Square index={index} key={index}/>

        ))
    );

    drawPlayAgain = () => {
        if (this.state.gameOver) {
            return (
                <button className="btn btn-primary mx-auto mt-4">
                    <i className="fas fa-redo"></i>
                    &nbsp;Play Again
                </button>)
        }
    }

}
// app/javascript/stylesheets/styles.scss
$width: 300px;
$height: 300px;
.canvas {
  width: $width;
  height: $height;
  text-align: center;

  .square {
    width: $width/3;
    line-height: $height/3;
    font-size: $height/6;
    height: $height/3;
    border: 1px solid grey;

    &.last-move {
      background-color: #2E2F30;
      color: white;
    }
  }
}

.user.nav-link {
  .nav-avatar {
    width: 30px;
    height: 30px;
    background-size: contain;
    background-position: center center;
    border: 2px solid #464646;
    display: inline-block;
    border-radius: 100%;
    position: relative;
    bottom: -10px;
  }
}

UI

New Look

Pumping life into the UI

// app/javascript/components/Canvas.jsx

import React from 'react';
import Alert from './bootstrap/Alert';
import NavBar from './bootstrap/NavBar';
import Square from './Square';
import SquareContext from './contexts';


const DefaultCanvasState = {
    players: [
        {
            name: 'Emma',
            symbol: 'X',
            avatar: 'https://image.shutterstock.com/image-photo/beautiful-woman-face-closeup-beauty-260nw-1403676473.jpg'
        },
        {name: 'Lisa', symbol: 'O', avatar: 'https://static3.bigstockphoto.com/8/7/2/large1500/278719948.jpg'}],
    gameOver: false,
    gamerSquares: [],
    winner: null,
    nextTurn: 0,
    lastMove: null,
    squares: Array(9).fill(null),

};


export default class Canvas extends React.Component {
    constructor(props) {
        super(props);
        this.playHandler = this.playHandler.bind(this);
        this.valid = this.valid.bind(this);
        this.invalid = this.invalid.bind(this);
        this.determineGameOver = this.determineGameOver.bind(this);

    }

    state = {...DefaultCanvasState, currentUser: DefaultCanvasState.players[0]};

    playHandler(squareId) {
        // validation if its permitted to click on this square
        if (this.invalid(squareId)) return;

        var squares = this.state.squares.slice(); // clones
        squares[squareId] = this.state.players[this.state.nextTurn].symbol;
        var nextTurn = this.state.nextTurn == 0 ? 1 : 0;
        var lastMove = squareId;
        var {winner, gameOver, gamerSquares}  = this.determineGameOver(squares);
        this.setState({squares, nextTurn, lastMove, gamerSquares, gameOver, winner})
    }

    determineGameOver = (squares) => {
        var that = this;
        var gameOver = false;
        var winner = null;
        var gamerSquares = [];
        var pattern = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]];
        pattern.forEach((arr) => {
            if (squares[arr[0]] == squares[arr[1]] && squares[arr[1]] == squares[arr[2]] && squares[arr[2]] != null) {
                gameOver = true;
                gamerSquares = arr;
                winner = that.state.nextTurn;
            }
        });

        // Game ends, all squares are filled but game is not over yet
        //  Mark the game-over and declare no body won
        if(!gameOver && squares.indexOf(null) == -1){
            gameOver = true;
            winner = null;
        }

        return {winner, gameOver, gamerSquares};
    };

    valid = (squareId) => {
        if (!this.state.gameOver && this.state.squares[squareId] == null)
            return true;
        return false
    };

    invalid = (squareId) => {
        return !this.valid(squareId);
    };

    renderHeader = () => {
        if (this.state.gameOver && this.state.winner != null) {
            return (
                <h6>
                    <span className="font-weight-bold">!! {this.state.players[this.state.winner].name}</span>
                    &nbsp; wins the game !!
                </h6>
            )
        } else if (!this.state.winner && this.state.gameOver)
        {
            return (
                <h6>
                    Game Over, but no body wins.
                </h6>
            )
        } else
        {
            return (
                <h6>
                    <span className="font-weight-bold">{this.state.players[this.state.nextTurn].name}</span>
                    &nbsp;has next turn to play!
                </h6>
            )
        }
    };

    startOver = ()=>{
        this.setState(DefaultCanvasState);
    };

    render = () => (
        <React.Fragment>
            <NavBar user={this.state.currentUser}/>
            <Alert/>
            <div className="container p-5 text-center">
                {this.renderHeader()}

                <div className="mt-4 row">
                    <div className="col-sm-4">
                        <h2 className="badge badge-primary p-4 border rounded-circle">{this.state.players[0].symbol}</h2>
                        <h2>Player1</h2>
                        <h3>{this.state.players[0].name}</h3>
                        {this.drawTurnPen(0)}
                    </div>

                    <div className="col-sm-4">
                        <div className="canvas d-flex flex-wrap mx-auto">
                            <SquareContext.Provider value={{state: this.state, handler: this.playHandler}}>
                                {this.genSquares()}
                            </SquareContext.Provider>
                            {this.drawPlayAgain()}
                        </div>
                    </div>

                    <div className="col-sm-4">
                        <h2 className="badge badge-primary p-4 border rounded-circle">{this.state.players[1].symbol}</h2>
                        <h2>Player2</h2>
                        <h3>{this.state.players[1].name}</h3>
                        {this.drawTurnPen(1)}
                    </div>
                </div>
            </div>
        </React.Fragment>
    );


    drawTurnPen = (playerIndex = 0) => {
        if (this.state.nextTurn == playerIndex) {
            return (
                <i className="fas fa-pen-fancy fa-5x  text-success border rounded-circle"></i>
            )
        }
    };

    genSquares = () => (
        this.state.squares.map((_data, index) => (
            <Square index={index} key={index}/>

        ))
    );

    drawPlayAgain = () => {
        if (this.state.gameOver) {
            return (
                <button onClick={this.startOver} className="btn btn-primary mx-auto mt-4">
                    <i className="fas fa-redo"></i>
                    &nbsp;Play Again
                </button>)
        }
    };

}
// app/javascript/components/Square.jsx

import React from 'react';
import SquareContext from './contexts'

// <!-- Since Squares are gonna have synamic value and events and gonna repeat -->
// <!-- We are going to create a separate component -->

export default class Square extends React.Component {
    static contextType = SquareContext;

    markActive = () => {
        if (this.props.index == this.context.state.lastMove) {
            return true;
        }else if(this.context.state.gamerSquares.indexOf(this.props.index) > -1){
            return true;
        }
        return false;
    };

    render = () => (
        <div className={`square ${this.markActive() ? 'last-move' : ''}`}
             onClick={() => {
                 this.context.handler(this.props.index)
             }}>
            {this.context.state.squares[this.props.index]}
        </div>
    )
}
 
Share this