Skip to main content
Tic-Tac-Toe.

Building a Tic-Tac-Toe web-app with Webpack + Babel + React + Redux

In this tutorial, you will learn how to build a modern web game and you will learn the basics of Webpack, Babel, React and Redux. The resulting repository is found here.

Our Stack

For creating a lightning fast Tic-Tac-Toe webgame, several things are important:

Run a local server

We will use Node.js for running our application on a local server. Don’t worry! This will be explained in detail.

Package manager

Our project will depend on Webpack, Babel, React and Redux. In order to install these dependencies, we will use a package manager called npm which is capable of downloading and installing the dependencies.

Optimize & Bundle files

We need to optimize and bundle our game files. If we would not do this, it can take twice as long to respond to an action or to load the application in the browser. Webpack does all of this for you!

Next generation JavaScript please!

There are a lot of impracticalities in good old JavaScript. One such example is JSX. JSX is an extension of JavaScript which enables you to create new HTML elements in JavaScript with lovely syntactic sugar. JSX is just one example, Babel will automatically compile the next generation JavaScript to the old JavaScript. Besides that, the Polyfill module of Babel makes sure that the code runs in any browser.

Virtual DOM

It is a very expensive operation to rerender the DOM completely. It is more efficient to use a Virtual DOM instead and only rerender the elements that are changed. React (originally developed by Facebook) will take care of this. Using React, we are able to efficiently change the DOM.

Atomic State Changes

In the past, application often sent their complete state to a server. It is better to only sent atomic state changes to a server. In the past, often the complete state was kept in memory, but nowadays, every action will emit one atomic state change. Why would you do this? One reason is that it enables you to implement a history. It is easy to keep track of all of the changes that are made and go back in time. Also, the cost for synchronization between two nodes reduces. It is better to start of with atomic state changes since it has a lot of advantages. Redux can do the job for you.

Setting up the application

Node.js and NPM

I recommend that you set up NPM and Node.js globally on your machine. Now, in order to setup our Tic-Tac-Toe project, go to a folder and execute the following command:

npm init

You will be asked a lot of questions, so feel free to answer them. If you don’t know the answer to a question, just hit the return button on the keyboard to continue. When this is done, npm stores the configuration in a filed called package.json. The package.json should now include something similar like this:

{
  "name": "tic-tac-toe",
  "version": "1.0.0",
  "description": "A simple Tic-Tac-Toe application.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Local Server

To serve the static files, I set up a local server using a package called serve-static. This can be installed by executing the following command:

npm install connect serve-static --save-dev

Then, I added a “serve” script to the npm configuration (package.json) in order to run the server:

...
  "scripts": {
    "serve": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
...

Also, I created a server.js file with the following contents:

var connect = require('connect');
var serveStatic = require('serve-static');
connect().use(serveStatic(__dirname + '/public/')).listen(8080, function () {
    console.log('Tic-Tac-Toe running on 8080...');
});

Now if you do “npm run-script serve”, the serve-static package is called. The command will now wait for requests. This package will then forward any request to the public folder. So, I created also a “public” folder and a “public/index.html” with the following contents:

<!doctype html>

<html lang="en">
<head>
    <meta charset="utf-8">

    <title>Tic-Tac-Toe</title>
    <meta name="description" content="Tic-Tac-Toe">
    <meta name="author" content="The Data-Blogger">

    <script src="./build/bundle.js"></script>
</head>

<body>
Hello World!
</body>
</html>

Also create the “public/build” folder since we will put our bundle file in here. Now if you did “npm run-script serve” and you go to “http://localhost:8080/index.html” in your browser, the “public/index.html” file will be shown!

Webpack

Installing Webpack is done by executing the following command:

npm install webpack --save-dev

The save-dev flag will add the package to the package.json file. For convenience, we will also install some loaders. These will be used to include CSS files in our bundle:

npm install css-loader style-loader --save-dev

We will now add webpack to the “start” script (in package.json) such that it runs whenever the “npm run-script start” command is executed:

...
  "main": "index.js",
  "scripts": {
    "serve": "node server.js",
    "start": "webpack --config webpack.config.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Kevin Jacobs",
  "license": "ISC",
  "devDependencies": {
...

Before we continue, we need to setup a webpack.config.js file. Make sure that it contains the following content:

module.exports = {
    entry: "./dev/entry.js",
    output: {
        filename: "./public/build/bundle.js"
    },
    module: {
        loaders: [
            { test: /\.css$/, loader: "style-loader!css-loader" }
        ]
    }
};

It simply converts all required styles to JavaScript and it reads the entry file in the “dev” folder (dev/entry.js). Please make an empty “dev/entry.js” file. It will bundle all JavaScript in the public/build/bundle.js file. This bundle file is already loaded by our index.html file.

Create a stylesheet

Now put the following contents in the dev/entry.js file:

require('./style.css');

Also create the dev/style.css file:

body {
    background-color: yellow;
}

Now try to compile the scripts into a bundle by calling “npm run-script start”. If this succeeded, try to run the server and check “http://localhost:8080/index.html”! You should see the following:

Hello World!

If you are still with me, you will now have the following folder structure:

Folder structure.

Conclusions

So Webpack bundled our beautiful style in the bundle.js (since the entry.js file required the style.css file). The index.html file simply loads the bundle.js file and the server only serves index.html (and bundle.js). Now we can continue with the Tic-Tac-Toe application!

We wrote a “serve” script and a “start” script. “npm run-script serve” will spawn a local server and “npm run-script start” will package all files into “bundle.js”. We have to run “npm run-script start” often to rebuild the “bundle.js” file (our you should implement a watch script). You should run “npm run-script serve” only once to spawn the server.

Installing remaining dependencies

Just execute the following to install the remaining dependencies:

npm install react react-dom redux babel-loader babel-core babel-preset-es2016 babel-preset-react react-redux redux-logger --save-dev

Now add the following loader to Webpack:

            {
                test: /.jsx?$/,
                loader: 'babel-loader',
                exclude: /node_modules/,
                query: {
                    presets: ['es2016', 'react']
                }
            }

This will transform JavaScript files with React syntax to plain old JavaScript.

Also

Building the application

index.html

We will first modify our index.html file. Give it the following contents:

<!doctype html>

<html lang="en">
<head>
    <meta charset="utf-8">

    <title>Tic-Tac-Toe</title>
    <meta name="description" content="Tic-Tac-Toe">
    <meta name="author" content="The Data-Blogger">
</head>

<body>
    <div id="root"></div>

    <script src="./build/bundle.js"></script>
</body>
</html>

Not that much is changed, we will keep it short and simple.

style.css

Just a few style changes. We will create a few components in a minute. In this stylesheet, we will apply some theming. The theming is not the main goal of the tutorial. Therefore, I will not explain how it works.

body, html {
    margin: 0;
}

body {
    font-family: Verdana, serif;
    background-color: #EEEEEE;
    font-size: 20px;
}

.grid, .panel {
    padding-top: 20px;
    width: 360px;
    text-align: center;
    margin: auto;
    clear: both;
}

.button {
    display: inline-block;
    padding: 20px 40px;
    background-color: #555555;
    color: #FFFFFF;
    cursor: pointer;
}

.cell {
    display: inline-block;
    float: left;
    background-color: #ff8600;
    width: 100px;
    height: 100px;
    margin: 10px;
    color: #FFFFFF;
    text-align: center;
    font-size: 40px;
    line-height: 100px;
    cursor: pointer;
}

.cell:nth-child(3n+1) {
    clear: left;
}

.flashline {
    display: block;
    width: 100%;
    font-size: 30px;
    line-height: 60px;
    text-align: center;
    background-color: #ff8600;
}

entry.js

We will now setup our entry.js. The file will depend on the TicTacToe game. Since we will implement Redux actions, we need a modified version of the game (called StatefulTicTacToe) which implements all these actions. This will be explained in a bit. Furthermore, we need to wrap it inside a “Provider” tag. This tag allows all child components to access the global state (the store). This store will be initialized in the store.js file. When everything is in place, the output is rendered to the “root” element as defined in index.html.

require('./style.css');

import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
import StatefulTicTacToe from './containers/statefultictactoe';
import { Provider } from 'react-redux'

ReactDOM.render(
    <Provider store={store}>
        <StatefulTicTacToe player="X" />
    </Provider>,
    document.getElementById('root')
);

Components

The first visible things we will create, are so called components. A component is an atomic front-end element. First, create the components folder inside the dev folder (./dev/components). Then, we will create four files inside this folder:

  • button.js – A simple button layout for the reset button which we will implement.
  • cell.js – The TicTacToe grid will consist of 9 cells. The cells are implemented here.
  • flashline.js – This will be a line displaying the status message (like “X has won the game!” or “Player X” or “It is a tie!”).

The contents of the files can be found here. I will walk you through one of the components, namely button.js. The button.js file has the following contents:

import React from 'react';

class Button extends React.Component {
    render() {
        return (
            <div onClick={this.props.onPress} className="button">{ this.props.label }</div>
        )
    }
}

export default Button;

This allows you to create a <Button /> HTML element. As you can see from the code, it has an “onPress” property (which is a function) and a “label” property. We can for example create the following element: <Button onPress={alert(‘Hi! You pressed me!’)} label=”Press me” />. It is just a mapping from actions to functions and it defines the style of the component.

Actions

Actions are atomic state changes. In our game, we will have two actions:

  • addMove(cell, player) – Adds a move in cell (0 [left-top], …, 8 [bottom-right]) for the given player.
  • resetGame() – Resets the game.

In Redux, an action has a required property type (which will be “ADD_MOVE” and “RESET” strings here respectively) and custom attributes. For the addMove action, we will have a cell and a player property. I have created an “actions” folder in the “dev” folder and a “game.js” file inside the “actions” folder (./dev/actions/game.js) with the following contents:

export const addMove = (cell, player) => {
    return {
        type: 'ADD_MOVE',
        cell: cell,
        player: player
    };
};

export const resetGame = () => {
    return {
        type: 'RESET'
    };
};

If you will implement everything, you can see the actions when playing the game in the developer console:

Tic-Tac-Toe.

As you can see, first an ADD_MOVE is executed for cell 0 (left-top) with player X and then an ADD_MOVE is executed at cell 1 (middle-top) for player O. We will need to implement all the rules to invalidate invalid moves. This will be explained in the next section.

Containers

Containers are compound components. We will define the following containers:

  • tictactoe.js – The rendering of the TicTacToe game.
  • resetbutton.js – A <Button /> with a press action which dispatches a game state change (using Redux).
  • statefultictactoe.js – Couples the actions of the TicTacToe game to state changes (using Redux) and implements the game logic.

These files can be found here. I now want to focus on the Redux implementation.

The resetbutton.js file simply dispatches a “RESET” action whenever the button is pressed. Also take a look at statefultictactoe.js:

const mapStateToProps = (state) => {
    return {
        player: state['player'],
        cells: state['cells'],
        message: getStatusMessage(state['cells'], state['player'])
    };
};

const mapDispatchToProps = (dispatch) => {
    return {
        onSetCell: (cell, cells, player) => {
            if (isValidMove(cells, cell)) dispatch(addMove(cell, player));
        },
        onReset: () => {
            dispatch(resetGame());
        }
    }
};

const statefulTicTacToe = connect(mapStateToProps, mapDispatchToProps)(TicTacToe);

Here, properties and actions are mapped onto the TicTacToe game. The tictactoe.js file has the following contents (I will display it here such that you can see how the properties and dispatchers are mapped):

import React from 'react';
import Cell from './cell';
import Button from './button';
import Flashline from './flashline';

class TicTacToe extends React.Component {
    render() {
        const game = this;
        return (
            <div>
                <Flashline message={this.props.message} />
                <div className="grid">
                    {
                        this.props.cells.map((value, cell) => (
                            <Cell key={cell} state={value} onPress={(evt) => {
                                game.props.onSetCell(cell, this.props.cells, this.props.player)
                            }}/>
                        ))
                    }
                </div>
                <div className="panel">
                    <Button label="Reset" onPress={(evt) => {
                        game.props.onReset()
                    }} />
                </div>
            </div>
        )
    }
}

export default TicTacToe

Whenever a <Cell /> is pressed, an “ADD_MOVE” action is dispatched. But only valid moves are allowed. When a move is invalid (for example, when a cell is already occupied), then no move will be dispatched at all and the game will remain in the same state. Reducers will listen for Redux actions, so that is what we will implement next.

Reducers

Reducers are specified in the ./dev/reducers/ folder. A reducer takes a state and an action and will return a new resulting state. It may never modify the original state! We will have the following files in here:

  • cells.js – This file will update the state of the cells and will respond to both ADD_MOVE and RESET.
  • player.js – This file updates the state of the player. Since every move results into a change of player, it will respond to ADD_MOVE. Whenever the game is reset, the player is also reset, so it must also respond to a RESET action.
  • index.js – Combines the reducers of cells.js and player.js.

Take a look at cells.js:

const cells = (state = [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined], action) => {
    switch (action.type) {
        case 'ADD_MOVE':
            return state.map((item, cell) => {
                return cell === action.cell ? action.player : item;
            });
        case 'RESET':
            return [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined];
        default:
            return state;
    }
};

export default cells;

It waits for an incoming action. When the incoming action is ADD_MOVE, it will set the played cell to the current player. Whenever a RESET action is dispatched, the cells will be set to undefined. In all other cases, the current state is just passed through.

Now look at player.js:

const player = (state = 'X', action) => {
    switch (action.type) {
        case 'ADD_MOVE':
            return (state === 'X') ? 'O' : 'X';
        case 'RESET':
            return 'X';
        default:
            return state;
    }
};

export default player;

You can see here that the player is toggled on an ADD_MOVE action. The player is set to X when the game is started over and by default, the current state is passed through.

These reducers are combined in index.js:

import cells from "./cells";
import player from './player';
import { combineReducers } from "redux";

const TicTacToeApp = combineReducers({cells, player});

export default TicTacToeApp;

That is it! Whenever an action is dispatched, both reducers will get called.

Store

Now we are able to implement a store. The store is just the global state. It is implemented as follows (in dev/store.js):

import TicTacToeApp from './reducers/index';
import { createStore } from 'redux';
import logger from 'redux-logger';
import { applyMiddleware } from 'redux';

let store = createStore(TicTacToeApp, applyMiddleware(logger));

export default store;

Also, logger middleware is attached to the store. This is useful for debugging. In the console, you will see all dispatched actions.

Now we have all code in place! I did not explain all straightforward code, but if you have any questions about any file, feel free to ask! You can build the game using “npm run-script start” and you can start the game by running “npm run-script serve” and by visiting “http://localhost:8080/” in your browser!

Why Redux at all?

Think about our flow. This now enables us to share the actions to other clients! We could even implement a client-server structure. So imagine two computers playing the Tic-Tac-Toe game and one server. Suppose that the first computer performs an action. The delta state (the action) is now send to the server. This server then broadcasts the action to the other client. The reducer in the other client picks up to state change and there we are! We distributed our global state by sending state changes.

Conclusion (TL;DR)

It takes some time to set up a proper web project, but it definitely pays of in the end (in terms of speed and reusability of components). If you followed all the steps correctly, you would end up with a game like this:

In-game image.

Kevin Jacobs

Kevin Jacobs

Kevin Jacobs is a certified Data Scientist and blog writer for Data Blogger. He is passionate about any project that involves large amounts of data and statistical data analysis. Kevin can be reached using Twitter (@kmjjacobs), LinkedIn or via e-mail: mail@kevinjacobs.nl.