Tictac Toe using React Context API with Ruby On Rails
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. 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.
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
withindex
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 line1
root 'homes#index'
In
layouts/application.html.erb
add line inhead
section1
<%= javascript_pack_tag 'hello_react', 'data-turbolinks-track': 'reload' %>
Then run rails server
bash rails s
localhost:3000
will get you1 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.

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>
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
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>
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>
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>
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
Tictac Toe Using React Context Api With Ruby On Rails
Shiva Bhusal (Software Engineer) 16 Min. Read Jul 23, 2019
ReactJS React Tic Tac Toe Game Nodejs Ruby On Rails Rails Context API JSX
Read More..