23
loading...
This website collects cookies to deliver better user experience
"type": "module"
in the root of the server-side package.json
.node -v
in your terminal, and if it is lower make sure to update it.dependencies
sections from both package.json
files will look alike"dependencies": {
"immutable": "^4.0.0-rc.12",
"redux": "^4.0.5",
"socket.io": "^2.3.0"
}
"dependencies": {
"react": "^16.13.0",
"react-dom": "^16.13.0",
"react-scripts": "0.9.x",
"socket.io-client": "^2.3.0"
}
Game
, Board
, and Square
. As you can imagine the Game
contains one Board
and the Board
contains nine Square
's. The state floats from the root Game
component through the Board
props down to the Square
's props.Square
is a pure component, it knows how to render itself based on the incoming props/data. Concept is very similar to pure functions. As a matter of fact, some components are pure functions.// .\front-end\index.js
function Square(props) {
return (
<button className="square" onClick={props.onClick}>
{props.value}
</button>
);
}
Board
is also pure component, it knows how to render squares and pass state down there.// .\front-end\index.js
class Board extends React.Component {
renderSquare(i) {
return (
<Square
value={this.props.squares[i]}
onClick={() => this.props.onClick(i)}
/>
);
}
render() {
return (
<div>
<div className="board-row">
{this.renderSquare(0)}
/* ... render 8 more squares */
</div>
</div>
);
}
}
Game
component. It holds the state, it calculates the winner, it defines what will happen, when user clicks on the square.// .\front-end\index.js
class Game extends React.Component {
constructor(props) {
super(props);
this.state = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
jumpTo(step) {
/* jump to step */
}
reset() {
/* reset */
}
handleClick(i) {
/* handle click on the square */
}
render() {
/* check if we have a winner and update the history */
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
</div>
);
}
}
const INITIAL_STATE = {
history: [{
squares: Array(9).fill(null),
}],
stepNumber: 0,
xIsNext: true,
};
}
const INITIAL_STATE = Map({
history: List([
Map({
squares: List([
null, null, null,
null, null, null,
null, null, null
]),
})]),
stepNumber: 0,
xIsNext: true,
winner: false
});
const newState = state.set('winner', true);
will produce new state object. How cool is that?PERFORM_MOVE
to perform a move, action will carry a box index that move was made for
JUMP_TO_STEP
to enable time-traveling, this action will carry step number to which the user wants to jump to
RESET
resets the whole game progress to the initial empty board// .\back-end\src\reducer.js
const INITIAL_STATE = Map({
history: List([
Map({
squares: List([
null, null, null,
null, null, null,
null, null, null
]),
})]),
stepNumber: 0,
xIsNext: true,
winner: false
});
...
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'PERFORM_MOVE':
/* todo */
case 'JUMP_TO_STEP':
/* todo */
case 'RESET':
/* todo */
}
return state;
}
PREFORM_MOVE
On every move we will first check if the move is legit, meaning that we might already have a winner and the game is over or the user tries to hit filled box. If any of these happens we will return the same state with no modifications.// .\back-end\src\reducer.js
function performMove(state, boxIndex){
const history = state.get('history');
const current = history.last();
let squares = current.get('squares');
let winner = state.get('winner');
if(winner || squares.get(boxIndex)) {
return state;
}
squares = squares.set(boxIndex, state.get('xIsNext') ? 'X' : 'O');
winner = calculateWinner(squares);
return state
.set('history', state
.get('history')
.push(Map({ squares: squares }))
)
.set('stepNumber', history.size)
.set('xIsNext', !state.get('xIsNext'))
.set('winner', winner);
}
JUMP_TO_STEP
To perform a time-travel we need to reverse the history to the step we want to move to and update current step number with a new value. And of course return new state.// .\back-end\src\reducer.js
function jumpToStep(state, step){
return state
.set('history', state.get('history').take(step + 1))
.set('stepNumber', step)
.set('xIsNext', (step % 2) === 0)
.set('winner', false);
}
RESET
Reset is pretty much like a JUMP_TO_STEP
, with only difference that we are jumping back to the very first step. After we are done, we return a new state.// .\back-end\src\reducer.js
function reset(state){
return state
.set('history', state.get('history').take(1))
.set('stepNumber', 0)
.set('xIsNext', true)
.set('winner', false);
}
// .\back-end\src\reducer.js
export default function reducer(state = INITIAL_STATE, action) {
switch (action.type) {
case 'PERFORM_MOVE':
return performMove(state, action.boxIndex);
case 'JUMP_TO_STEP':
return jumpToStep(state, action.step);
case 'RESET':
return reset(state);
}
return state;
}
// .\back-end\index.js
import redux from 'redux';
import server from './src/server.js';
import reducer from './src/reducer.js';
const store = redux.createStore(reducer);
server.startServer(store);
state-changed
event that we will emit on the server and subscribe to on the client. And we need to have an action
event that we will emit on the client and subscribe to it on the server.connection
event that will be triggered every time a new connection appears. So all we need is subscribe to it.state-change
event on that socket to transfer the latest state from the Redux Store. Then we will also subscribe to an action
event from the same socket and once an event will arrive we will dispatch the whole object into the Redux Store. That'll provide a complete setup for the new socket connection.state-change
event to all connected sockets// ..\back-end\src\server.js
function(store) {
console.log("Let the Game begin");
const io = new Server().attach(8090);
store.subscribe(
() => io.emit('state-change', store.getState().toJS())
);
io.on('connection', (socket) => {
console.log('New Connection');
socket.emit('state-change', store.getState().toJS());
socket.on('action', store.dispatch.bind(store));
});
}
state-changed
event for that matter and pass received state execute the ReactDOM.render(<Game gameState={newState} />, ...);
. Don't worry, calling ReactDOM.render() multiple times, absolutely fine from the performance perspective, it will have the same performance implication as calling setState
inside the component.dispatch
callback which takes action
object as a parameter and emit an action
event through the socket connection.// .\front-end\index.js
const socket = io("http://localhost:8090");
socket.on('state-change', state =>
ReactDOM.render(
<Game
dispatch={(action) => socket.emit('action', action)}
gameState={state}
/>,
document.getElementById('root')
)
);
Square
and Board
components were already pure, now it is just a matter of making the root component, Game
pure as well.// .\front-end\index.js
/* Square and Board were not changed */
class Game extends React.PureComponent {
jumpTo(step) {
this.props.dispatch({type: 'JUMP_TO_STEP', step});
}
reset() {
this.props.dispatch({type: 'RESET'});
}
handleClick(boxIndex) {
this.props.dispatch({type: 'PERFORM_MOVE', boxIndex: boxIndex})
}
render() {
const { history, stepNumber, xIsNext, winner } = this.props.gameState
const current = history[stepNumber];
const status = winner
? 'Winner: ' + winner
: 'Next player: ' + (xIsNext ? 'X' : 'O');
const moves = history.map((step, move) => {
/* time travelling */
});
return (
<div className="game">
<div className="game-board">
<Board
squares={current.squares}
onClick={(i) => this.handleClick(i)}
/>
</div>
<div className="game-info">
<div><button onClick={() => this.reset()}>Reset the Game</button></div>
<div>{status}</div>
<ol>{moves}</ol>
</div>
</div>
);
}
}