Tutorial

Creating A Simple Shopping Cart with React.js and Flux

Draft updated on Invalid Date
author

Ken Wheeler

Creating A Simple Shopping Cart with React.js and Flux

This tutorial is out of date and no longer maintained.

Introduction

Welcome to the fourth and final installment of the Learning React series! Up to this point, we’ve learned how React’s API allows us to create rich stateful components, how to use them in practice & how Facebook’s Flux architecture works.

Today we are going to put all of it together to create a basic shopping cart application. In a typical e-commerce website, the product detail page has several moving parts that rely on one another, and React really helps simplify and organize the co-dependency between them.

If you haven’t already, I highly recommend checking out the first three parts of this series:

ReactJS 0.12

During the writing of this series, React released version 0.12 which made some fairly significant changes. This tutorial will be written in 0.12 syntax. Some of the changes include:

  • The /** @jsx React.DOM */ header is no longer required when writing JSX syntax
  • renderComponent is now render
  • renderComponentToString is now renderToString

You can check out the entire changelog on the official blog post.

Getting Started

Our first step to architecting an application is defining what it should do. We want to:

  • Display a product with several options
  • Change the price when selecting an option
  • Add items to the cart
  • Remove items from the cart
  • Display the number of items in the cart
  • Display the total price of the items in the cart
  • Display the total price for each option in the cart based upon quantity
  • Change our “Add To Cart” button caption to “Sold Out” and disable it when inventory is depleted for a given option
  • Display the cart after adding a product or clicking the “View Cart” button

This is what our finished product should look like:

This app is going to be purely client-side, so we aren’t going to need a server. Instead, we will be using a mock API and mock data in order to focus on the components themselves. Let’s take a look at our directory structure:

Directory Structure

    css/
    ---- app.css
    img/
    ---- scotch-beer.png
    js/
    ---- actions/
    -------- FluxCartActions.js     // Our app's action creators
    ---- components/
    -------- FluxCart.react.js      // Cart Component
    -------- FluxCartApp.react.js   // Main Controller View
    -------- FluxProduct.react.js   // Product Component
    ---- constants/
    -------- FluxCartConstants.js   // Our app's action constants
    ---- dispatcher/
    -------- AppDispatcher.js       // Our app's dispatcher
    ---- stores/
    -------- CartStore.js           // Cart Store
    -------- ProductStore.js            // Product Store
    ---- utils/
    -------- CartAPI.js             // Mock API
    ---- app.js                     // Main app.js file
    ---- ProductData.js             // Mock Data
    index.html
    package.json

Below, check out our package.json file. We will be using the following modules:

  • Browserify
  • Reactify
  • React
  • Flux
  • Watchify
  • Uglify
  • Underscore
  • Envify

We can run npm install to install all of our dependencies, and then use the npm start command to start a process that watches our project and bundles our source on save.

package.json

    {
      "name": "flux-pricing",
      "version": "0.0.1",
      "description": "Pricing component with flux",
      "main": "js/app.js",
      "dependencies": {
        "flux": "^2.0.0",
        "react": "^0.12.0",
        "underscore": "^1.7.0"
      },
      "devDependencies": {
        "browserify": "~6.2.0",
        "envify": "~3.0.0",
        "react": "^0.12.0",
        "reactify": "^0.15",
        "watchify": "~2.1.0"
      },
      "scripts": {
        "start": "watchify -o js/bundle.js -v -d .",
        "build": "browserify . | uglifyjs -cm > js/bundle.min.js"
      },
      "author": "Ken Wheeler",
      "browserify": {
        "transform": [
          "reactify",
          "envify"
        ]
      }
    }

API and Mock Data

In the interests of keeping us focused on Flux and React, we will be using a mock API and mock data for our product that we are going to display. That said, writing our product data and our API similarly to the way we would work with a real API adds the benefit that we could have an easier time plugging in a real API down the road if we had wanted to.

Let’s have a look at what our sample Product Data looks like:

ProductData.js

    module.exports = {
      // Load Mock Product Data Into localStorage
      init: function() {
        localStorage.clear();
        localStorage.setItem('product', JSON.stringify([
          {
            id: '0011001',
            name: 'Scotch.io Signature Lager',
            image: 'scotch-beer.png',
            description: 'The finest lager money can buy. Hints of keyboard aerosol, with a whiff of iKlear wipes on the nose. If you pass out while drinking this beverage, Chris Sevilleja personally tucks you in.',
            variants: [
              {
                sku: '123123',
                type: '40oz Bottle',
                price: 4.99,
                inventory: 1

              },
              {
                sku: '123124',
                type: '6 Pack',
                price: 12.99,
                inventory: 5
              },
              {
                sku: '1231235',
                type: '30 Pack',
                price: 19.99,
                inventory: 3
              }
            ]
          }
        ]));
      }

    };

As you can see above, we define a product that has options called variants. Our schema mirrors the type of data you might typically get back from a call to a restful API. We go ahead and load this data into localStorage, so that our mock API can grab that data and load it into our app.

See below how our mock API grabs our data from localStorage and then uses Flux actions to send that data to our ProductStore:

CartAPI.js

    var FluxCartActions = require('../actions/FluxCartActions');

    module.exports = {

      // Load mock product data from localStorage into ProductStore via Action
      getProductData: function() {
        var data = JSON.parse(localStorage.getItem('product'));
        FluxCartActions.receiveProduct(data);
      }

    };

So now that we have our sample product data, and a sample API call, how do we use this to bootstrap our application with “server” data?

It’s really as simple as just initializing our data, running our API call, and then mounting our controller view. Our main app.js file, shown below, is responsible for this process:

app.js

    window.React = require('react');
    var ProductData = require('./ProductData');
    var CartAPI = require('./utils/CartAPI')
    var FluxCartApp = require('./components/FluxCartApp.react');

    // Load Mock Product Data into localStorage
    ProductData.init();

    // Load Mock API Call
    CartAPI.getProductData();

    // Render FluxCartApp Controller View
    React.render(
      <FluxCartApp />,
      document.getElementById('flux-cart')
    );

Dispatcher

Since we are using the Flux architecture for this application, we are going to need to create our own instance of Facebook’s Dispatcher library. We also add a handleAction helper method to our Dispatcher instance, so that we can identify where this action came from.

While this isn’t explicitly required for our current application, if we wanted to hook into a real API or handle actions from places other than views, it is nice to have this architecture in place if we needed to handle those actions differently than those that originated from views.

AppDispatcher.js

    var Dispatcher = require('flux').Dispatcher;

    // Create dispatcher instance
    var AppDispatcher = new Dispatcher();

    // Convenience method to handle dispatch requests
    AppDispatcher.handleAction = function(action) {
      this.dispatch({
        source: 'VIEW_ACTION',
        action: action
      });
    }

    module.exports = AppDispatcher;

In our handleAction method, we receive an action from an action creator and then have our Dispatcher dispatch the action with a source property and the action that was supplied as an argument.

Actions

Now that we have our dependencies, data, and our Dispatcher set up, it’s time to start working on our project’s functional requirements. Actions are a great place to start. Let’s define some action constants in order to define which actions our app will perform:

FluxCartConstants.js

    var keyMirror = require('react/lib/keyMirror');

    // Define action constants
    module.exports = keyMirror({
      CART_ADD: null,       // Adds item to cart
      CART_REMOVE: null,    // Remove item from cart
      CART_VISIBLE: null,   // Shows or hides the cart
      SET_SELECTED: null,   // Selects a product option
      RECEIVE_DATA: null    // Loads our mock data
    });

After defining our constants, we need to create their corresponding action creator methods. These are methods that we can call from our views/components that will tell our Dispatcher to broadcast the action to our Stores.

The action itself consists of an object containing our desired action constant and a data payload. Our Stores then update and fire change events, which our Controller View listens to in order to know when to begin a state update.

Below, check out how we use our Dispatcher’s handleAction method to pass an actionType constant and associated data to our Dispatcher:

FluxCartActions.js

    var AppDispatcher = require('../dispatcher/AppDispatcher');
    var FluxCartConstants = require('../constants/FluxCartConstants');

    // Define actions object
    var FluxCartActions = {

      // Receive inital product data
      receiveProduct: function(data) {
        AppDispatcher.handleAction({
          actionType: FluxCartConstants.RECEIVE_DATA,
          data: data
        })
      },

      // Set currently selected product variation
      selectProduct: function(index) {
        AppDispatcher.handleAction({
          actionType: FluxCartConstants.SELECT_PRODUCT,
          data: index
        })
      },

      // Add item to cart
      addToCart: function(sku, update) {
        AppDispatcher.handleAction({
          actionType: FluxCartConstants.CART_ADD,
          sku: sku,
          update: update
        })
      },

      // Remove item from cart
      removeFromCart: function(sku) {
        AppDispatcher.handleAction({
          actionType: FluxCartConstants.CART_REMOVE,
          sku: sku
        })
      },

      // Update cart visibility status
      updateCartVisible: function(cartVisible) {
        AppDispatcher.handleAction({
          actionType: FluxCartConstants.CART_VISIBLE,
          cartVisible: cartVisible
        })
      }

    };

    module.exports = FluxCartActions;

Stores

Now that we have our Actions defined, it is time to create our Stores. Each Store manages application state for a domain within an application, so we are going to create one for our product and one for our cart. Let’s start with our ProductStore:

ProductStore.js

    var AppDispatcher = require('../dispatcher/AppDispatcher');
    var EventEmitter = require('events').EventEmitter;
    var FluxCartConstants = require('../constants/FluxCartConstants');
    var _ = require('underscore');

    // Define initial data points
    var _product = {}, _selected = null;

    // Method to load product data from mock API
    function loadProductData(data) {
      _product = data[0];
      _selected = data[0].variants[0];
    }

    // Method to set the currently selected product variation
    function setSelected(index) {
      _selected = _product.variants[index];
    }


    // Extend ProductStore with EventEmitter to add eventing capabilities
    var ProductStore = _.extend({}, EventEmitter.prototype, {

      // Return Product data
      getProduct: function() {
        return _product;
      },

      // Return selected Product
      getSelected: function(){
        return _selected;
      },

      // Emit Change event
      emitChange: function() {
        this.emit('change');
      },

      // Add change listener
      addChangeListener: function(callback) {
        this.on('change', callback);
      },

      // Remove change listener
      removeChangeListener: function(callback) {
        this.removeListener('change', callback);
      }

    });

    // Register callback with AppDispatcher
    AppDispatcher.register(function(payload) {
      var action = payload.action;
      var text;

      switch(action.actionType) {

        // Respond to RECEIVE_DATA action
        case FluxCartConstants.RECEIVE_DATA:
          loadProductData(action.data);
          break;

        // Respond to SELECT_PRODUCT action
        case FluxCartConstants.SELECT_PRODUCT:
          setSelected(action.data);
          break;

        default:
          return true;
      }

      // If action was responded to, emit change event
      ProductStore.emitChange();

      return true;

    });

    module.exports = ProductStore;

Above, we define two private methods, loadProductData and setSelected. We use loadProductData to, unsurprisingly, load our mock product data into our _product object. Our setSelected method is used to set which product variant is currently selected.

We expose this data using the public methods getProduct and getSelected, which return their respective internal objects. These methods can be called after require’ing our Store within a view.

Lastly, we register a callback to our AppDispatcher that uses a switch statement to determine if the supplied payload matches an action we want to respond to. In the event that it does, we call our private methods with the supplied action data, and fire a change event, forcing our view to retrieve the new state and update its display.

Next up, let’s create our CartStore:

    var AppDispatcher = require('../dispatcher/AppDispatcher');
    var EventEmitter = require('events').EventEmitter;
    var FluxCartConstants = require('../constants/FluxCartConstants');
    var _ = require('underscore');

    // Define initial data points
    var _products = {}, _cartVisible = false;

    // Add product to cart
    function add(sku, update) {
      update.quantity = sku in _products ? _products[sku].quantity + 1 : 1;
      _products[sku] = _.extend({}, _products[sku], update)
    }

    // Set cart visibility
    function setCartVisible(cartVisible) {
      _cartVisible = cartVisible;
    }

    // Remove item from cart
    function removeItem(sku) {
      delete _products[sku];
    }

    // Extend Cart Store with EventEmitter to add eventing capabilities
    var CartStore = _.extend({}, EventEmitter.prototype, {

      // Return cart items
      getCartItems: function() {
        return _products;
      },

      // Return # of items in cart
      getCartCount: function() {
        return Object.keys(_products).length;
      },

      // Return cart cost total
      getCartTotal: function() {
        var total = 0;
        for(product in _products){
          if(_products.hasOwnProperty(product)){
            total += _products[product].price * _products[product].quantity;
          }
        }
        return total.toFixed(2);
      },

      // Return cart visibility state
      getCartVisible: function() {
        return _cartVisible;
      },

      // Emit Change event
      emitChange: function() {
        this.emit('change');
      },

      // Add change listener
      addChangeListener: function(callback) {
        this.on('change', callback);
      },

      // Remove change listener
      removeChangeListener: function(callback) {
        this.removeListener('change', callback);
      }

    });

    // Register callback with AppDispatcher
    AppDispatcher.register(function(payload) {
      var action = payload.action;
      var text;

      switch(action.actionType) {

        // Respond to CART_ADD action
        case FluxCartConstants.CART_ADD:
          add(action.sku, action.update);
          break;

        // Respond to CART_VISIBLE action
        case FluxCartConstants.CART_VISIBLE:
          setCartVisible(action.cartVisible);
          break;

        // Respond to CART_REMOVE action
        case FluxCartConstants.CART_REMOVE:
          removeItem(action.sku);
          break;

        default:
          return true;
      }

      // If action was responded to, emit change event
      CartStore.emitChange();

      return true;

    });

    module.exports = CartStore;

Above, we laid out our store similarly to the way we laid out our ProductStore. We use the _products object to store the products that are currently in our cart, and the _cartVisibility boolean to define the current visibility status of our cart.

We added some more complex public methods that allow our Controller View to retrieve application state:

  • getCartItems - Returns the items in our cart
  • getCartCount - Returns a count of how many items are in our cart
  • getCartTotal - Returns the total price of all of our cart items

Now that we have our Stores built, it’s time to get our hands dirty and build out our Views.

Controller View

Our Controller View is our top-level component that listens for changes on our stores and then updates our application’s state by calling our Store’s public methods. This state is then passed down to child components via props.

The Controller View is responsible for:

  • Setting our applications state by calling Store’s public methods
  • Composing child components and passing state properties via props
  • Listening for Store’s change events

FluxCartApp.react.js

    var React = require('react');
    var CartStore = require('../stores/CartStore');
    var ProductStore = require('../stores/ProductStore');
    var FluxProduct = require('./FluxProduct.react');
    var FluxCart = require('./FluxCart.react');

    // Method to retrieve state from Stores
    function getCartState() {
      return {
        product: ProductStore.getProduct(),
        selectedProduct: ProductStore.getSelected(),
        cartItems: CartStore.getCartItems(),
        cartCount: CartStore.getCartCount(),
        cartTotal: CartStore.getCartTotal(),
        cartVisible: CartStore.getCartVisible()
      };
    }

    // Define main Controller View
    var FluxCartApp = React.createClass({

      // Get initial state from stores
      getInitialState: function() {
        return getCartState();
      },

      // Add change listeners to stores
      componentDidMount: function() {
        ProductStore.addChangeListener(this._onChange);
        CartStore.addChangeListener(this._onChange);
      },

      // Remove change listers from stores
      componentWillUnmount: function() {
        ProductStore.removeChangeListener(this._onChange);
        CartStore.removeChangeListener(this._onChange);
      },

      // Render our child components, passing state via props
      render: function() {
        return (
          <div className="flux-cart-app">
            <FluxCart products={this.state.cartItems} count={this.state.cartCount} total={this.state.cartTotal} visible={this.state.cartVisible} />
            <FluxProduct product={this.state.product} cartitems={this.state.cartItems} selected={this.state.selectedProduct} />
          </div>
        );
      },

      // Method to setState based upon Store changes
      _onChange: function() {
        this.setState(getCartState());
      }

    });

    module.exports = FluxCartApp;

We start by creating a public method called getCartState. We use this method to call public methods on Stores to retrieve their current state and set our applications state with the results. We call this method first during the getInitialState method, and also when a Store’s change event is received.

In order to receive these change events, we add listeners on our Stores during the mounting process, so that we know when they change. We remove these events when/if our component is unmounted.

In our render method, we compose our component using the FluxCart and FluxProduct components. Here, we pass our state props down to them using component properties, or props.

Product View

It’s time to get down to the meat and potatoes of this app, and that is our Product view. We want to take the props that got passed from our Controller View and make a rich, interactive product display.

Let’s get this party started.

FluxProduct.react.js

    var React = require('react');
    var FluxCartActions = require('../actions/FluxCartActions');

    // Flux product view
    var FluxProduct = React.createClass({

      // Add item to cart via Actions
      addToCart: function(event){
        var sku = this.props.selected.sku;
        var update = {
          name: this.props.product.name,
          type: this.props.selected.type,
          price: this.props.selected.price
        }
        FluxCartActions.addToCart(sku, update);
        FluxCartActions.updateCartVisible(true);
      },

      // Select product variation via Actions
      selectVariant: function(event){
        FluxCartActions.selectProduct(event.target.value);
      },

      // Render product View
      render: function() {
        var ats = (this.props.selected.sku in this.props.cartitems) ?
          this.props.selected.inventory - this.props.cartitems[this.props.selected.sku].quantity :
          this.props.selected.inventory;
        return (
          <div className="flux-product">
            <img src={'img/' + this.props.product.image}/>
            <div className="flux-product-detail">
              <h1 className="name">{this.props.product.name}</h1>
              <p className="description">{this.props.product.description}</p>
              <p className="price">Price: ${this.props.selected.price}</p>
              <select onChange={this.selectVariant}>
                {this.props.product.variants.map(function(variant, index){
                  return (
                    <option key={index} value={index}>{variant.type}</option>
                  )
                })}
              </select>
              <button type="button" onClick={this.addToCart} disabled={ats  > 0 ? '' : 'disabled'}>
                {ats > 0 ? 'Add To Cart' : 'Sold Out'}
              </button>
            </div>
          </div>
        );
      },

    });

    module.exports = FluxProduct;

Prior to our render method, we define Action methods that we bind to elements in our component. By importing our Actions, we can then call them from these methods, and kick off the update process:

  • selectProduct - Sets which product option is currently selected
  • addToCart - Adds the currently selected product to the cart and opens the cart

Inside of our render method, we calculate how many units of the selected product are available to sell (ats) by checking how many are in the cart vs the selected products inventory. We use this to toggle the “Add To Cart” button’s state.

Cart View

Now, we need a cart. So let’s put one together. In our app, when a product is added to the cart, a single line item represents the selected option. The quantity of that option can be increased, but it won’t create new line items. Instead, the quantity being purchased is displayed and the line item’s price adjusts accordingly.

Let’s make this happen:

FluxCart.react.js

    var React = require('react');
    var FluxCartActions = require('../actions/FluxCartActions');

    // Flux cart view
    var FluxCart = React.createClass({

      // Hide cart via Actions
      closeCart: function(){
        FluxCartActions.updateCartVisible(false);
      },

      // Show cart via Actions
      openCart: function(){
        FluxCartActions.updateCartVisible(true);
      },

      // Remove item from Cart via Actions
      removeFromCart: function(sku){
        FluxCartActions.removeFromCart(sku);
        FluxCartActions.updateCartVisible(false);
      },

      // Render cart view
      render: function() {
        var self = this, products = this.props.products;
        return (
          <div className={"flux-cart " + (this.props.visible ? 'active' : '')}>
            <div className="mini-cart">
              <button type="button" className="close-cart" onClick={this.closeCart}>×</button>
              <ul>
                {Object.keys(products).map(function(product){
                  return (
                    <li key={product}>
                      <h1 className="name">{products[product].name}</h1>
                      <p className="type">{products[product].type} x {products[product].quantity}</p>
                      <p className="price">${(products[product].price * products[product].quantity).toFixed(2)}</p>
                      <button type="button" className="remove-item" onClick={self.removeFromCart.bind(self, product)}>Remove</button>
                    </li>
                  )
                })}
              </ul>
              <span className="total">Total: ${this.props.total}</span>
            </div>
            <button type="button" className="view-cart" onClick={this.openCart} disabled={Object.keys(this.props.products).length > 0 ? "" : "disabled"}>View Cart ({this.props.count})</button>
          </div>
        );
      },

    });

    module.exports = FluxCart;

And now we have a cart! Our cart component has three event methods:

  • closeCart - Close the cart
  • openCart - Opens the cart
  • removeFromCart - Removes item from the cart and closes the cart

When rendering our cart, we use the map method to render out our line items. Notice on the <li> tag, we added the key attribute. This is a special attribute used when adding dynamic children to a component. It is used internally in React to uniquely identify said children so that they retain the proper order and state during reconciliation. If you remove this and open your console, you can see that React will throw a warning telling you that key hasn’t been set, and you will likely experience rendering anomalies.

For our cart’s hide and show functionality, all that we are doing is adding and removing an active class, and letting CSS do the rest.

Conclusion

If you have been following along with the source or building from scratch, hit index.html and you can see our app in action. Otherwise, check out the link to the demo below. Add items to the cart until inventory depletes to see our buttons’ states change and the totals update in the cart.

Don’t stop there though, take the demo source and try to add some new features to our cart, like a product grid display using react-router or adding more than 1 selectable option per product. The sky’s the limit folks.

This marks the end of the Learning React series, and I hope everybody has had as much fun learning it as I did writing it. I’m of the firm belief that 2015 will be the year of React, so take what you have learned here, get out there and build some cool stuff.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about our products

About the authors
Default avatar
Ken Wheeler

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
Leave a comment


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Become a contributor for community

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

DigitalOcean Documentation

Full documentation for every DigitalOcean product.

Resources for startups and SMBs

The Wave has everything you need to know about building a business, from raising funding to marketing your product.

Get our newsletter

Stay up to date by signing up for DigitalOcean’s Infrastructure as a Newsletter.

New accounts only. By submitting your email you agree to our Privacy Policy

The developer cloud

Scale up as you grow — whether you're running one virtual machine or ten thousand.

Get started for free

Sign up and get $200 in credit for your first 60 days with DigitalOcean.*

*This promotional offer applies to new accounts only.