I was told to code a board game app using React for a job interview.
They told me they chose other candidates to move forward with, so I was wondering on what I could improve.
The full code can be viewed here.
Here are the more interesting components:
TicTacToeGame
import React, {useCallback} from 'react'; // useCallback imported by mistake here..
import './tic-tac-toe.scss';
import update from 'immutability-helper';
import Board from '@/board/board';
import ScoreList from '@/score-list/score-list';
import {isGameOver, isDraw} from '../../utils/tic-tac-toe';
import x from '#/images/x.svg';
import o from '#/images/o.svg';
import restart from '#/images/restart.svg';
const X = 0;
const O = 1;
const EMPTY = 2;
const xImg = <img className="xo" src={x}/>;
const oImg = <img className="xo" src={o}/>;
const IMAGE_MAP = {
[X]: xImg,
[O]: oImg
};
const INITIAL_STATE = {
turn: 0,
players: [{
score: 0
}, {
score: 0
}],
board: [
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY],
[EMPTY, EMPTY, EMPTY]
]
};
class TicTacToe extends React.Component {
constructor(props) {
super(props);
this.state = INITIAL_STATE;
}
renderBox = (row, column) => {
const {board} = this.state;
const value = board[row][column];
if (value === EMPTY) {
return null;
}
return IMAGE_MAP[value];
};
boxClickHandler = (row, column) => {
const {turn, players, board} = this.state;
const value = board[row][column];
if (value === EMPTY) {
const newBoard = update(board, {[row]: {$splice: [[column, 1, turn % 2]]}});
if (isGameOver(newBoard, row, column)) {
// React will batch both setStates since it's in onClick event handler
const newPlayers = update(players, {
[turn % 2]: {
score: {
$apply: function (s) {
return s + 1;
}
}
}
});
this.setState({
turn: INITIAL_STATE.turn,
players: newPlayers,
board: INITIAL_STATE.board
});
} else if (isDraw(board, turn)) {
this.setState({
turn: INITIAL_STATE.turn,
players,
board: INITIAL_STATE.board
});
}
else {
this.setState({
turn: turn + 1,
board: newBoard
});
}
}
};
resetGame = () => {
this.setState({
...INITIAL_STATE
});
};
render() {
const {players, board} = this.state;
return <div className="tic-tac-toe">
<div className="tic-tac-toe-row">
<div className="tic-tac-toe-wrapper">
<h1 className="title">
Tic - Tac - Toe
</h1>
</div>
</div>
<div className="tic-tac-toe-row">
<div className="tic-tac-toe-wrapper">
<Board
board={board}
width={400}
height={400}
isGameOver={isGameOver}
boxTemplate={this.renderBox}
onBoxClick={this.boxClickHandler}/>
</div>
</div>
<div className="tic-tac-toe-row">
<div className="tic-tac-toe-wrapper">
<div className="scores-and-reset">
<ScoreList
width={340}
height={100}
players={players}/>
<button className="reset-button" onClick={this.resetGame}>
Reset
<img src={restart}/>
</button>
</div>
</div>
</div>
</div>;
}
}
export default TicTacToe;
Board
import React, {useCallback} from 'react';
import './board.scss';
import Box from '@/box/box';
import classNames from 'classnames';
const Board = ({board, width, height, boxTemplate, onBoxClick}) => {
return <div className="board" style={{width, height}}>
{board.map((row, i) => <div key={i} className="row" style={{height: height / board.length}}>
{row.map((column, j) => {
const boxClass = classNames({
top: i !== 0,
right: j !== row.length - 1,
bottom: i !== board.length - 1,
left: j !== 0
});
const memoizedBoxClickCallback = useCallback(
(e) => {
onBoxClick(i, j);
},
[i, j],
);
return <Box key={i + j} className={boxClass} width={width / row.length} height={height / board.length} onClick={memoizedBoxClickCallback}>
{boxTemplate(i, j)}
</Box>;
})}
</div>
)}
</div>;
};
export default Board;
Box
import React from 'react';
import './box.scss';
class Box extends React.PureComponent {
render() {
const {children, className, width, height, onClick} = this.props;
return <div className={`box ${className} pointer`} style={{width, height}} onClick={onClick}>
{children}
</div>;
}
}
export default Box;
Design choices:
I tried to make the base components reusable for other implementation of board games so I pass
onClick
handler for theBox
component andboxTemplate
function as a children to theBox
component to render whatever the game wants (e.g TicTacToeGame will render XO respectively, SnakesNLaddersGames will render snakes and ladders respectively and so on..), also theBoard
component sets classtop
right
left
bottom
respectively (depends on where the box is relative to the edge of the board) without styles for eachBox
so the game component it-self could decide on the styling of these components based on the location of theBox
.I didn't use any state management library as this is a pretty small project. I know adding such library could benefit me in the future for when the application expands, but right now I don't see a reason for the state logic update (which is inside the function
boxClickHandler
inTicTacToeGame
) to be inside a reducer function (even if usinguseReducer
hook) because this logic is not a reusable logic for other board games that might get implemented using the baseBoard
andBox
components.
The only thing I didn't took into account is that the functional component Board
might get rendered for nothing if a game component state which doesn't require board update will change, but since I implemented only TicTacTacGame
and all its state requires re-render of the Board
component I didn't took this into account. If I were to solve this I would just use PureComponenet
or React.memo
on the component.