Tictac Toe using React Context API with Ruby On Rails

16 Min. Read
Jul 23, 2019

 ReactJS React Tic Tac Toe Game Nodejs Ruby On Rails Rails Context API JSX

Today we will see how to build a beautiful tic-tac-toe game.

For Ruby On Rails Developers

  • Install Ruby
  • Install Latest Rails Gem.
1
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.

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

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

    1
    
      <%= javascript_pack_tag 'hello_react', 'data-turbolinks-track': 'reload' %>
    
  • Then run rails server bash rails s

  • localhost:3000 will get you

    1
    2
    3
    4
    5
    
    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.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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;
1
2
3
4
// app/javascript/components/contexts.jsx
import React from 'react';
var SquareContext = React.createContext({});
export default SquareContext;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 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>
    )
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 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>)
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// 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>)
        }
    };

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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>
    )
}

You might also like

You can simply transfer state from parent to child or use Context API to transfer model/context data to underlying components. But the later one is easier to maintain.
Read More..


Write To Me

Hire me shiva bhusal
We'll never share your email with anyone else.
I'll never share your email with anyone else.