Tictac Toe using React Context API with Ruby On Rails
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
withindex
action, because without a controller you wont be able to use the app.rails g controller homes index
In
config/routes.rb
add a lineroot 'homes#index'
In
layouts/application.html.erb
add line inhead
section<%= javascript_pack_tag 'hello_react', 'data-turbolinks-track': 'reload' %>
- Then run rails server
rails s
localhost:3000
will get youHomes#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.
<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>
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
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>
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>)
}
};
}
// 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>
)
}