ReactJS and Flux

Posted by David Leonard on February 15, 2015

With ReactJS becoming one of the hottest JavaScript libraries in late 2014, I decided to investigate the hype. What better way to do so then by building an actual application using ReactJS and the Flux architecture, while documenting my process for others to learn?

Project Directory

To demonstrate and explore ReactJS + Flux, we will build a simple shopping cart application. Below is an overview of what our project structure will look like:

flux
├── dist
│   ├── index.html
│   └── js
│       └── main.js
├── gulpfile.js
├── package.json
└── src
    ├── index.html
    └── js
        ├── actions
        │   └── app-actions.js
        ├── components
        │   ├── app-addtocart.js
        │   ├── app-cart.js
        │   ├── app-catalog.js
        │   ├── app-decrease.js
        │   ├── app-increase.js
        │   ├── app-removefromcart.js
        │   └── app.js
        ├── constants
        │   └── app-constants.js
        ├── dispatchers
        │   ├── app-dispatcher.js
        │   └── dispatcher.js
        ├── main.js
        └── stores
            └── app-store.js

For those interested in simply viewing the finalized project, you may find the source code here.

Environment setup

For this project, we will be using npm to install various packages, as well as gulp for task automation. Below is the package.json file that contains all dependencies needed for this project:

{
  "name": "flux",
  "version": "0.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "gulp": "^3.8.11",
    "gulp-browserify": "^0.5.1",
    "gulp-concat": "^2.4.3",
    "react": "^0.12.2",
    "reactify": "^1.0.0"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

Once you have package.json in your root directory, simply run:

~/flux λ npm install
Tip: For OS X users, you will have to run sudo npm install.

If you haven’t had exposure to using gulp, use this as your first starting point.

/************************
*------gulpfile.js------*
************************/

var gulp = require('gulp');
var browserify = require('gulp-browserify');
var concat = require('gulp-concat');

Here, we require the modules we will need within our gulpfile.js. ReactJS supports what is known as JSX, which is a JavaScript-HTML hybrid syntax. We create a gulp task to transform this using reactify for all of our JavaScript files, which will be concatanted into a single file: main.js within the dist folder.

 
gulp.task('browserify', function(){
	gulp.src('src/js/main.js')
		.pipe(browserify({transform: 'reactify'}))
		.pipe(concat('main.js'))
		.pipe(gulp.dest('dist/js'));
});

Next up, we also copy our index.html into the dist folder:

gulp.task('copy', function(){
	gulp.src('src/index.html')
		.pipe(gulp.dest('dist'));
});

We now tell grunt which tasks to load:

gulp.task('default', ['browserify', 'copy']);

When running gulp in our terminal, the above tasks will be executed - our dist folder will contain our production files which have been converted using reactify.

Lastly, let us configure a watch task which will check for changes in our project files and update the page accordingly:

gulp.task('watch', function(){
	gulp.watch('src/**/*.*', ['default']);
});

Running gulp watch will continually check for changes to your project files.

Putting it all together, this is what our gulpfile.js is looking like:

/************************
*------gulpfile.js------*
************************/

var gulp = require('gulp');
var browserify = require('gulp-browserify');
var concat = require('gulp-concat');

gulp.task('browserify', function(){
	gulp.src('src/js/main.js')
		.pipe(browserify({transform: 'reactify'}))
		.pipe(concat('main.js'))
		.pipe(gulp.dest('dist/js'));
});

gulp.task('copy', function(){
	gulp.src('src/index.html')
		.pipe(gulp.dest('dist'));
});

gulp.task('default', ['browserify', 'copy']);

gulp.task('watch', function(){
	gulp.watch('src/**/*.*', ['default']);
});

Great! Now we have our environment completely set up. Let’s jump into the application.

Lastly, we create the directories needed within our project.

~/flux λ mkdir src
~/flux λ cd src
~/flux/src λ mkdir js
~/flux/src/js λ mkdir actions components constants dispatchers stores


Index.html

Our first step in building our application will be building our index.html file, which will be our application entry point.

~/flux/src λ vim index.html
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<link rel="stylesheet" media="screen" href="http://netdna.bootstrapcdn.com/bootstrap/3.1.0/css/bootstrap.min.css">

</head>
<body>
<div id="main" class="containter"></div>
<script src="js/main.js"></script>
</body>
</html>

Here we have included Bootstrap for tables, a div with id=main, which will be where we mount our React components to from main.js. Now that this part is finished, we move onto building our central app.js file.

Main.js and App.js

The purpose of these files is to tie in all of our components that will be needed. For our shopping cart application we will be building two major components: an item catalog as well as an item cart, which will both be defined towards the end of our application.

~/flux/src/js λ vim main.js
/************************
*--------main.js--------*
************************/

/** @jsx React.DOM */
var APP = require('./components/app');
var React = require('react');

React.renderComponent(
  <APP />,
  document.getElementById('main'));
~/flux/src/js/components λ vim app.js
/************************
*--------app.js---------*
************************/

/** @jsx React.DOM */
var React = require('react
');
var Catalog = require('../components/app-catalog.js');
var Cart = require('../components/app-cart.js');

var APP = 
	React.createClass({
		render: function(){
			return (
				<div>
				<h1> Lets Shop </h1>
				<Catalog />
				<h1>Cart</h1>
				<Cart />
				</div>
			)
		}
	});

module.exports = APP;


Introducing Flux

Flux is not to be confused with a framework, it is closer to a design pattern in which we have a unidirectional data flow throughout our application. This simplifies our logic and allows us to build web applications which will scale over time no matter how complicated and numerous our views and models (from MVC standpoints) may grow.

flux architecture

Over the course of this tutorial, we will cover each of the key components that make up Flux as we develop our application.

Dispatchers

The role of the dispatcher within the Flux model is to prevent race conditions, which it does by queueing up all events in our application as promises, and will execute them in the order in which they are received. The first piece of code is actually the boilerplate dispatcher.js file provided by the Facebook team:

~/flux/src/js/dispatchers λ vim dispatcher.js
/************************
*-----dispatcher.js-----*
************************/

var Promise = require('es6-promise').Promise;
var merge = require('react/lib/merge');

var _callbacks = [];
var _promises = [];

/**
 * Add a promise to the queue of callback invocation promises.
 * @param {function} callback The Store's registered callback.
 * @param {object} payload The data from the Action.
 */
var _addPromise = function(callback, payload) {
  _promises.push(new Promise(function(resolve, reject) {
    if (callback(payload)) {
      resolve(payload);
    } else {
      reject(new Error('Dispatcher callback unsuccessful'));
    }
  }));
};

/**
 * Empty the queue of callback invocation promises.
 */
var _clearPromises = function() {
  _promises = [];
};

var Dispatcher = function() {};
Dispatcher.prototype = merge(Dispatcher.prototype, {

  /**
   * Register a Store's callback so that it may be invoked by an action.
   * @param {function} callback The callback to be registered.
   * @return {number} The index of the callback within the _callbacks array.
   */
  register: function(callback) {
    _callbacks.push(callback);
    return _callbacks.length - 1; // index
  },

  /**
   * dispatch
   * @param  {object} payload The data from the action.
   */
  dispatch: function(payload) {
    _callbacks.forEach(function(callback) {
      _addPromise(callback, payload);
    });
    Promise.all(_promises).then(_clearPromises);
  }

});

module.exports = Dispatcher;

We will need to install es6-promises to use within dispatcher.js. Let’s go ahead and do that:

npm install es6-promises
Note: For OS X users, you will have to run sudo npm install es6-promises.
Note: Keep track of the merge library, which we will use to extend the method Dispatcher.prototype with additional functionality throughout this project.

With this boilerplate out of the way, we build our own app-dispatcher, which will be responsible for queueing up the incoming actions that our application can take.

~/flux/src/js/dispatchers λ vim app-dispatcher.js
/************************
*---app-dispatcher.js---*
************************/

var Dispatcher = require('./dispatcher.js');
var merge  = require('react/lib/merge');

var AppDispatcher = merge(Dispatcher.prototype, {
  handleViewAction: function(action){
    console.log('action', action);
    this.dispatch({
      source: 'VIEW_ACTION',
      action: action
    })
  }
})

module.exports = AppDispatcher;

Within handleViewAction, we build an object containing the action to take through action: action. Also notice how we used merge to extend the Dispatcher.prototype method from dispatcher.js with this new method.

With our dispatchers now out of the way, we move onto defining these actions which can occur in our application. Before we can do so, let us define some action constants within constants/app-constants.js:

~/flux/src/js/constants λ vim app-constants.js
/************************
*----app-constants.js---*
************************/

module.exports = {
	ADD_ITEM: 'ADD_ITEM',
	REMOVE_ITEM: 'REMOVE_ITEM',
	INCREASE_ITEM: 'INCREASE_ITEM',
	DECREASE_ITEM: 'DECREASE_ITEM',
};


Actions

An Action within the Flux architecture is nothing more than an event which will get propogated through the Dispatcher, which will tell the Store how to react. We will define what a Store is a little later, but for now let us define all possible actions that can occur in a shopping application:

~/flux/src/js/actions λ vim app-actions.js
/************************
*----app-actions.js-----*
************************/

var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');

var AppActions = {

}

module.exports = AppActions;

Here we require the modules we need to define our actions with, and we declare an object AppActions which will contain all our methods that will be executed based on the action.

var AppActions = {
	addItem:function(item){
		AppDispatcher.handleViewAction({
		  actionType: AppConstants.ADD_ITEM,
		  item: item
		})
	},
}

We first define our addItem method, which takes an item as a parameter. We then call our AppDispatcher.handleViewAction and pass in an object containing the type of action that the Store will need to take, as well as the corresponding item which will be added.

var AppActions = {
  removeItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.REMOVE_ITEM,
      index: index
    })
  },
}

Here we define a similar action to addItem, removeItem which takes an index of the item which we will remove from our Cart which we will define later. Next we define two similar methods, decreaseItem and increaseItem, which both take an index as the parameter.

  decreaseItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.DECREASE_ITEM,
      index: index
    })
  },
  increaseItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.INCREASE_ITEM,
      index: index
    })
  }

Great! We have now defined all the actions that can occur in our shopping application. Let’s check out what our app-actions.js is looking like now:

/************************
*----app-actions.js-----*
************************/

var AppConstants = require('../constants/app-constants.js');
var AppDispatcher = require('../dispatchers/app-dispatcher.js');

var AppActions = {
  addItem:function(item){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.ADD_ITEM,
      item: item
    })
  },
  removeItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.REMOVE_ITEM,
      index: index
    })
  },
  decreaseItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.DECREASE_ITEM,
      index: index
    })
  },
  increaseItem:function(index){
    AppDispatcher.handleViewAction({
      actionType: AppConstants.INCREASE_ITEM,
      index: index
    })
  }
}

module.exports = AppActions;

With our actions defined, we move onto the next main component of our application: the Store.

Stores

Stores in Flux react to events (actions). The Store registers what events it is listening for with the Dispatcher.

Note: Stores look like a controller, but are actually closer to a service in AngularJS.

We begin with our app-store.js:

~/flux/src/js/stores λ vim app-store.js
/************************
*-----app-store.js------*
************************/

var AppDispatcher = require('../dispatchers/app-dispatcher');
var AppConstants = require('../constants/app-constants');
var merge = require('react/lib/merge');
var EventEmitter = require('events').EventEmitter;
Note: We are using the EventEmitter method from NodeJS, which will come in handy when broadcasting that an action has occured to the Store and to the subsequent Components, which we will implement shortly.
var CHANGE_EVENT = "change";

Here we define a CHANGE_EVENT variable, which will simply save us some writing in the future. We will use this to signal when an action has taken place and that the Store needs to act accordingly.

Next we will define some dummy items for our actual store (not to be confused with the Store) in the Flux architecture. Since we are not using any sort of external database or API, we will put these items here.

var _catalog = [];

for(var i=1; i<9; i++){
  _catalog.push({
    'id': 'Widget' +i,
    'title':'Widget #' + i,
    'summary': 'This is an awesome widget!',
    'description': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus, commodi.',
    'img': '/assets/product.png',
    'cost': i
  });
}

Next up we define the actual methods which handle the data within our cart, which stores the items we have chosen to purchase.

var _cartItems = [];


function _removeItem(index){
  _cartItems[index].inCart = false;
  _cartItems.splice(index, 1);
}

function _increaseItem(index){
  _cartItems[index].qty++;
}

function _decreaseItem(index){
  if(_cartItems[index].qty>1){
    _cartItems[index].qty--;
  }
  else {
    _removeItem(index);
  }
}

function _addItem(item){
  if(!item.inCart){
    item['qty'] = 1;
    item['inCart'] = true;
    _cartItems.push(item);
  }
  else {
    _cartItems.forEach(function(cartItem, i){
      if(cartItem.id === item.id){
        _increaseItem(i);
      }
    });
  }
}

function _cartTotals(){
  var qty =0, total = 0;
  _cartItems.forEach(function(cartItem){
    qty += cartItem.qty;
    total += cartItem.qty*cartItem.cost;
  });
  return {'qty': qty, 'total': total};
}

For the most part, these mthods should be self-explanatory. We now implement our actual AppStore, which will merge more functionality into dispatcher.js by extending the NodeJS eventEmitter method.

var AppStore = merge(EventEmitter.prototype, {
  emitChange: function(){
    this.emit(CHANGE_EVENT)
  },

  addChangeListener: function(callback){
    this.on(CHANGE_EVENT, callback)
  },

  removeChangeListener: function(callback){
    this.removeListener(CHANGE_EVENT, callback)
  },
})

module.exports = AppStore;
addChangeListener: This allows the Components to register with the Store, and the Store will listen for changes and will signal the appropriate callback method based on the action that has taken place.

The three methods above are important for registering when an event has been triggered, and we use callback methods in response to these events. Before we do the actual registration with the dispatcher, we will define some methods for getting data from our cart, catalog, and our item totals:

getCart: function(){
	return _cartItems
},

getCatalog: function(){
	return _catalog
},

getCartTotals: function(){
	return _cartTotals();
},

Lastly, we will register events that our Store will listen to with the Dispatcher. To do so, we will perform a switch case based on the action that has been received.

dispatcherIndex: AppDispatcher.register(function(payload){
    var action = payload.action; // this is our action from handleViewAction
    switch(action.actionType){
      case AppConstants.ADD_ITEM:
        _addItem(payload.action.item);
        break;

      case AppConstants.REMOVE_ITEM:
        _removeItem(payload.action.index);
        break;

      case AppConstants.INCREASE_ITEM:
        _increaseItem(payload.action.index);
        break;

      case AppConstants.DECREASE_ITEM:
        _decreaseItem(payload.action.index);
        break;
    }
    AppStore.emitChange();

    return true;
  })
Note: It is important to notice that we return true at the end of our code. Keep in mind that the Dispatcher queues up a chain of promises, and we need to return true in order for these promises to be resolved.
Pro-tip: We provide the Dispatcher with an index in the event that we have multiple stores, we would like to keep track of which Store is trying to register an action with the Dispatcher.

Putting it all together, our app-store.js is looking like this:

/************************
*-----app-store.js------*
************************/

var AppDispatcher = require('../dispatchers/app-dispatcher');
var AppConstants = require('../constants/app-constants');
var merge = require('react/lib/merge');
var EventEmitter = require('events').EventEmitter;

var CHANGE_EVENT = "change";

var _catalog = [];

for(var i=1; i<9; i++){
  _catalog.push({
    'id': 'Widget' +i,
    'title':'Widget #' + i,
    'summary': 'This is an awesome widget!',
    'description': 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ducimus, commodi.',
    'img': '/assets/product.png',
    'cost': i
  });
}

var _cartItems = [];


function _removeItem(index){
  _cartItems[index].inCart = false;
  _cartItems.splice(index, 1);
}

function _increaseItem(index){
  _cartItems[index].qty++;
}

function _decreaseItem(index){
  if(_cartItems[index].qty>1){
    _cartItems[index].qty--;
  }
  else {
    _removeItem(index);
  }
}


function _addItem(item){
  if(!item.inCart){
    item['qty'] = 1;
    item['inCart'] = true;
    _cartItems.push(item);
  }
  else {
    _cartItems.forEach(function(cartItem, i){
      if(cartItem.id === item.id){
        _increaseItem(i);
      }
    });
  }
}

function _cartTotals(){
  var qty =0, total = 0;
  _cartItems.forEach(function(cartItem){
    qty += cartItem.qty;
    total += cartItem.qty*cartItem.cost;
  });
  return {'qty': qty, 'total': total};
}


var AppStore = merge(EventEmitter.prototype, {
  emitChange: function(){
    this.emit(CHANGE_EVENT)
  },

  addChangeListener: function(callback){
    this.on(CHANGE_EVENT, callback)
  },

  removeChangeListener: function(callback){
    this.removeListener(CHANGE_EVENT, callback)
  },

  getCart: function(){
    return _cartItems
  },

  getCatalog: function(){
    return _catalog
  },

  getCartTotals: function(){
    return _cartTotals();
  },

  dispatcherIndex: AppDispatcher.register(function(payload){
    var action = payload.action; // this is our action from handleViewAction
    switch(action.actionType){
      case AppConstants.ADD_ITEM:
        _addItem(payload.action.item);
        break;

      case AppConstants.REMOVE_ITEM:
        _removeItem(payload.action.index);
        break;

      case AppConstants.INCREASE_ITEM:
        _increaseItem(payload.action.index);
        break;

      case AppConstants.DECREASE_ITEM:
        _decreaseItem(payload.action.index);
        break;
    }
    AppStore.emitChange();

    // Return true, this is a chain of promises that we need to resolve	
    return true;
  })
})

module.exports = AppStore;

Phew. That wraps up the implementation of the Store. Lastly, we move to our Components, which we have alluded to previously.

Components

React Components are essentially our views which grab state from the Store and pass it down through props to the child components. Components update whenever the events occur in our system through the Store via the Dispatcher. Since our view listens to the Store, it knows when the application state has changed and will update accordingly.

With that said, let us build our first React Component: app-addtocart.js:

~/flux/src/js/components λ vim app-addtocart.js
/************************
*----app-addtocart.js---*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppActions = require('../actions/app-actions.js');

var AddToCart = 
	React.createClass({
		handleClick: function(){
			AppActions.addItem(this.props.item);
		},

		render: function(){
			return <button onClick={this.handleClick}> Add to cart </button>
		}
	});

module.exports = AddToCart;

Here we use React.createClass to create a new Component with two methods: handleClick which will call the addItem action via AppActions and pass the item in to be added to our cart using this.props.item. We also define a render method which will render a button to add a selected item into our cart. Similarly, we define app-removefromcart.js, app-increase.js and app-decrease.js:

~/flux/src/js/components λ vim app-removefromcart.js
/************************
*-app-removefromcart.js-*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppActions = require('../actions/app-actions.js');

var RemoveFromCart = 
	React.createClass({
		handleClick: function(){
			AppActions.removeItem(this.props.index);
		},

		render: function(){
			return <button onClick={this.handleClick}> x </button>
		}
	});

module.exports = RemoveFromCart;
~/flux/src/js/components λ vim app-increase.js
/************************
*----app-increase.js----*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppActions = require('../actions/app-actions.js');

var Increase = 
	React.createClass({
		handleClick: function(){
			AppActions.increaseItem(this.props.index);
		},

		render: function(){
			return <button onClick={this.handleClick}> + </button>
		}
	});

module.exports = Increase;
~/flux/src/js/components λ vim app-decrease.js
/************************
*----app-decrease.js----*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppActions = require('../actions/app-actions.js');

var Decrease = 
	React.createClass({
		handleClick: function(){
			AppActions.decreaseItem(this.props.index);
		},

		render: function(){
			return <button onClick={this.handleClick}> - </button>
		}
	});

module.exports = Decrease;
Note: It is important to notice that these components do not inherit from a parent, they simply inherit from appActions. This is due to how data flows through the Flux architecture.

With these Components implemented, we now move onto the two big Components: app-catalog.js and app-cart.js, which control and display the items we can purchase and the items we currently want to buy, respectively. We start with app-catalog.js:

~/flux/src/js/components λ vim app-catalog.js
/************************
*-----app-catalog.js----*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppStore = require('../stores/app-store.js');
var AddToCart = require('../components/app-addtocart.js');

function getCatalog(){
	return {items: AppStore.getCatalog()}
}

We have defined a single method so far: getCatalog which returns a new object containing the set of items within the item catalog.

Next, we define our actual Catalog, which will contain two methods: getInitialState for getting the items within our item catalog, and a render method for displaying these items.

var Catalog = 
	React.createClass({
		getInitialState: function(){
			return getCatalog();
		},

		render: function(){
			var items = this.state.items.map(function(item){
				return <tr><td>{item.title}</td>
					       <td>${item.cost}</td>
					       <td><AddToCart item={item} /></td>
					   </tr>
			})
			return (
				<table className="table table-hover">
				{items}
				</table>
			)
		}
	});

module.exports = Catalog;

Putting it together, app-catalog.js is shown below:

/************************
*-----app-catalog.js----*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppStore = require('../stores/app-store.js');
var AddToCart = require('../components/app-addtocart.js');

function getCatalog(){
	return {items: AppStore.getCatalog()}
}

var Catalog = 
	React.createClass({
		getInitialState: function(){
			return getCatalog();
		},

		render: function(){
			var items = this.state.items.map(function(item){
				return <tr><td>{item.title}</td>
					       <td>${item.cost}</td>
					       <td><AddToCart item={item} /></td>
					   </tr>
			})
			return (
				<table className="table table-hover">
				{items}
				</table>
			)
		}
	});

module.exports = Catalog;

Lastly, we define our app-cart.js, which handles displaying our cart as well as the items within it.

~/flux/src/js/components λ vim app-cart.js
/************************
*------app-cart.js------*
************************/

/** @jsx React.DOM */
var React = require('react');
var AppStore = require('../stores/app-store.js');
var RemoveFromCart = require('../components/app-removefromcart.js');
var Increase = require('../components/app-increase.js');
var Decrease = require('../components/app-decrease.js');

function cartItems(){
	return {items: AppStore.getCart()}
}

Like our app-catalog.js, we also define a method for getting the items within our cart through the cartItems method.

var Cart = 
	React.createClass({
		getInitialState: function(){
			return cartItems();
		},

		componentWillMount: function(){
			AppStore.addChangeListener(this.onChange);
		},

		onChange: function(){
			this.setState(cartItems())
		},

		render: function(){
			var total = 0;
			var items = this.state.items.map(function(item, i){
				var subtotal = item.cost * item.qty;
				total += subtotal; 
				return (
					<tr key={i}>
						<td><RemoveFromCart index={i} /></td>
						<td>{item.title}</td>
						<td>{item.qty}</td>
						<td>
							<Increase index={i} />
							<Decrease index={i} />
						</td>
						<td>${subtotal}</td>
					</tr>
				)
			})
			return (
				<table className="table table-hover">
					<thead>
						<tr>
							<th></th>
							<th>Item</th>
							<th>Qty</th>
							<th></th>
							<th>Subtotal</th>
						</tr>
					</thead>
					<tbody>
						{items}
					</tbody>
					<tfoot>
						<tr>
							<td colSpan="4" className="text-right">Total</td>
							<td>${total}</td>
						</tr>
					</tfoot>
				</table>
			)
		}
	});

module.exports = Cart;

Most of the code above should look familiar with the exception of the componentWillMount method. Our Cart Component needs to listen for change events from the catalog. This method will handle this by calling the onChange method, which in turn resets out state to the items we currently have in our Cart, which will in turn get re-rendered via render.

Wrapping up

If you’ve made it this far, congratulations! Go ahead and run gulp in the project root directory and open up dist/index.html. You will be able to add, remove and increase/decrease the quanity of items in your cart.


← ImpactJS Tutorial Full-stack JavaScript + Github API →