Coding Tidbits

Cube Root is an exclusive Ruby on Rails Consultancy. Find our programming ramblings here.

Angular PhoneCat Tutorial in React JS

In this tutorial, we are going to re-implement the Angular JS PhoneCat tutorial in React JS.

PhoneCat Tutorial

PhoneCat is a catalog of Android devices, which you can filter, sort and see details of the device.

alt text

In this 12 step tutorial you will learn about:

  1. React JS
  2. Creating components
  3. Components interaction
  4. Routing in React

After you complete all the steps in this tutorial, you’ll have an app that looks something like this:

About React JS

React JS for the uninitiated, is a javascript framework from Facebook/Instagram. Most commonly used as a V in MVC. React is used in production by FaceBook, Instagram, Khanacademy, Atom editor and many more.

React uses Virtual DOM, an in-memory representation of the browser DOM, making it possible to re-render our entire app on every data change. Just like the good old 90’s!

Developing apps in React requires different thinking than other typical javascript frameworks.

React JS Primer

Components Not Templates

Enough treating Views as strings. React uses javascript objects (called components) to represent the app markup. Components facilitate separation of concerns and can be unit tested in isolation.

React components are created with React.createClass method. Every component must implement a render method.

Here is a very simple Hello component.

Hello = React.createClass({
  render: function(){
    return React.DOM.div(null, "Hello")
  }
})

React.renderComponent(Hello(), document.body);

As you can imagine, React.renderComponent renders our Hello component in document.body.

JSX

XML like HTML markup inside javascript is called JSX. JSX makes it easy for non-developers (for example designers) to edit the components. JSX is converted to javascript by the JSX transformer. /** @jsx React.DOM */ comment, signifies a jsx file, so JSX transformer can act on it.

For example

/** @jsx React.DOM */
Hello = React.createClass({
  render: function(){
    return <div>Hello</div>
  }
})

gets converted to

Hello = React.createClass({
  render: function(){
    return React.DOM.div(null, "Hello")
  }
})

Head over to displaying-data for a more complete primer.

Step-0 Bootstrapping

Tutorial sources are available at reactjs-phonecat.

We are using react-starter-template for the tutorial. node.js is used as local server and gulp.js as build tool.

To get strated, clone the repo and execute following commands in the project directory

$ git clone https://github.com/girishso/reactjs-phonecat#
$ cd reactjs-phonecat
$ git checkout -f step-0
$ npm install
$ bower install

Once successfully done, you can run the server by executing

$ gulp dev

Goto http://127.0.0.1:4000/ in your browser. You should see a success message.

Step-1 Mockups

Let’s start with the mockups of the actual pages we are going to create.

# To reset workspace
$ git checkout -f step-1

We have added html mockups, stylesheets and images. You can see the mockups here

http://127.0.0.1:4000/mock_index.html

http://127.0.0.1:4000/mock_show.html

PhoneCat components

Our next task is identifying the components. This is a very subjective topic and everybody can have different opinions. Here are the components I’ve decided to go with for the Home page.

alt text

Here is our components hierarchy -

- PhoneCatWrapper (blue)
  - PhoneCat (yellow)
    - SearchForm (red)
    - PhonesList (orange)
      - Phone (green)

Step-2 PhoneCatWrapper component

# To reset workspace
$ git checkout -f step-2

Let’s modify index.html, and use it as layout for the app.

src/index.html

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>React Phone Gallery</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <link rel="stylesheet" href="assets/vendor/bootstrap.css" />
  <link rel="stylesheet" href="assets/main.css" />
</head>
<body>
  <div id="app" class="container">
  </div>
  <script type="text/javascript" src="assets/main.js"></script>
</body>
</html>

main.js is the entry point of our app. It renders the PhoneCatWrapper component in #id

src/scripts/main.js

/** @jsx React.DOM */
require("script!react/react-with-addons.js")
require("script!jquery/jquery.js")
var PhoneCatWrapper = require("./components/PhoneCatWrapper.js")
React.renderComponent(<PhoneCatWrapper />, document.getElementById('app'))

We have copied contents of div.row from mock_index.html in the render function of PhoneCatWrapper.

src/scripts/components/PhoneCatWrapper.js

/** @jsx React.DOM */
var PhoneCatWrapper = React.createClass({
  render: function() {
    return (
      <div class="row">
        <div class="col-md-2">
          Search:
          <input type="text"/>
          Sort by:
          <select >
            <option value="name">Alphabetical</option>
            <option value="age">Newest</option>
          </select>
        </div>
        <div class="col-md-10">
          <ul class="phones">
          <li class="thumbnail phone-listing">
            <a href="#/phones/motorola-xoom-with-wi-fi" class="thumb"><img src="images/phones/motorola-xoom-with-wi-fi.0.jpg"></a>
            <a href="#/phones/motorola-xoom-with-wi-fi" >Motorola XOOM with Wi-Fi</a>
            <p>The Next, Next Generation

              Experience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb).
            </p>
          </li>
          <!-- code deleted for brevity -->
        </ul>
        </div>
      </div>
    )
  }
});

module.exports = PhoneCatWrapper;

Right now if you refresh the browser, it fails saying Line 19: Expected corresponding XJS closing tag for img. JSX needs strict XML. Let’s add the img closing tags.

Now it’s working but the layout is all messed up. This is because JSX expects HTML attributes in a slightly different manner, the culprit here is class it’s not showing up in the rendered HTML. To fix this replace all class attributes with className.

Now it’s working as expected!

Step-3 Extract all static components

# To reset workspace
$ git checkout -f step-3

To speedup the things, let’s extract all the remaining components from PhoneCatWrapper. Note that all these components have static html.

You won’t notice any difference in the UI, it’s the same old html rendered in browser. But we have modularized our code, more maintainable.

We intend to use PhoneCatWrapper as a thin wrapper having state around PhoneCat component.

src/scripts/components/PhoneCatWrapper.js

var PhoneCat = require("./PhoneCat.js")

var PhoneCatWrapper = React.createClass({
  render: function() {
    return (
      <PhoneCat />
    )
  }
});

PhoneCat is composed of SearchForm and PhonesList components.

src/scripts/components/PhoneCat.js

/** @jsx React.DOM */
var PhonesList = require("./PhonesList.js")
var SearchForm = require("./SearchForm.js")

var PhoneCat = React.createClass({
  render: function() {
    return (
      <div className="row">
        <div className="col-md-2">
          <SearchForm />
        </div>
        <div className="col-md-10">
          <PhonesList />
        </div>
      </div>
    )
  }
});

module.exports = PhoneCat;

PhonesList includes multiple Phone components.

src/scripts/components/PhonesList.js

/** @jsx React.DOM */
var Phone = require("./Phone.js")

var PhonesList = React.createClass({
  render: function() {
    return (
      <ul className="phones">
        <Phone />
        <Phone />
      </ul>
    )
  }
});

module.exports = PhonesList;

Phone represents… well a phone!

src/scripts/components/Phone.js

/** @jsx React.DOM */

var Phone = React.createClass({
  render: function() {
    return (
      <li className="thumbnail phone-listing">
        <a href="#/phones/motorola-xoom-with-wi-fi" className="thumb"><img src="images/phones/motorola-xoom-with-wi-fi.0.jpg" /></a>
        <a href="#/phones/motorola-xoom-with-wi-fi" >Motorola XOOM with Wi-Fi</a>
        <p>The Next, Next Generation

          Experience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb).
        </p>
      </li>
    )
  }
});

module.exports = Phone;

Finally, the SearchForm.

src/scripts/components/SearchForm.js

/** @jsx React.DOM */

var SearchForm = React.createClass({
  render: function() {
    return (
      <div>
        Search:
        <input type="text"/>
        Sort by:
        <select >
          <option value="name">Alphabetical</option>
          <option value="age">Newest</option>
        </select>
      </div>
    )
  }
});

module.exports = SearchForm;

Setp-4 Add dynamism

# To reset workspace
$ git checkout -f step-4

Enough of static components, let’s make ‘em dynamic!

Enter props (short for properties), props is how components talk to each other.

Our API is going to return JSON that looks like PHONES array.

src/scripts/components/PhoneCatWrapper.js

/** @jsx React.DOM */
var PhoneCat = require("./PhoneCat.js")

var PHONES = [
    {
        "age": 0,
        "id": "motorola-xoom-with-wi-fi",
        "imageUrl": "images/phones/motorola-xoom-with-wi-fi.0.jpg",
        "name": "Motorola XOOM\u2122 with Wi-Fi",
        "snippet": "The Next, Next Generation\r\n\r\nExperience the future with Motorola XOOM with Wi-Fi, the world's first tablet powered by Android 3.0 (Honeycomb)."
    },
    {
        "age": 1,
        "id": "motorola-xoom",
        "imageUrl": "images/phones/motorola-xoom.0.jpg",
        "name": "MOTOROLA XOOM\u2122",
        "snippet": "The Next, Next Generation\n\nExperience the future with MOTOROLA XOOM, the world's first tablet powered by Android 3.0 (Honeycomb)."
    },
    {
        "age": 2,
        "carrier": "AT&T",
        "id": "motorola-atrix-4g",
        "imageUrl": "images/phones/motorola-atrix-4g.0.jpg",
        "name": "MOTOROLA ATRIX\u2122 4G",
        "snippet": "MOTOROLA ATRIX 4G the world's most powerful smartphone."
    },
    {
        "age": 3,
        "id": "dell-streak-7",
        "imageUrl": "images/phones/dell-streak-7.0.jpg",
        "name": "Dell Streak 7",
        "snippet": "Introducing Dell\u2122 Streak 7. Share photos, videos and movies together. It\u2019s small enough to carry around, big enough to gather around."
    }
]

var PhoneCatWrapper = React.createClass({
  render: function() {
    return (
      <PhoneCat phones={PHONES} />
    )
  }
});

We are passing PhoneCat a property phones and assigning it to the PHONES array. In JSX you need to include javascript code inside the curly braces.

src/scripts/components/PhoneCat.js

var PhoneCat = React.createClass({
  render: function() {
    return (
      <div className="row">
        <div className="col-md-2">
          <SearchForm />
        </div>
        <div className="col-md-10">
          <PhonesList phones={this.props.phones} />
        </div>
      </div>
    )
  }
});

In PhoneCat we are passing phones property to PhonesList. You can access props inside the component as this.props.property_name

src/scripts/components/PhonesList.js

var PhonesList = React.createClass({
  render: function() {
    var phones = this.props.phones.map(function(phone, i){
      return <Phone phone={phone} key={i} />
    });

    return (
      <ul className="phones">
        {phones}
      </ul>
    )
  }
});

In PhonesList render function, we are creating an array phones of Phone components using the javascript map function. Then we are rendering that array. This is how you typically deal with a collection of components in React.

key property is used to uniquely identify the child components.

src/scripts/components/Phone.js

var Phone = React.createClass({
  render: function() {
    var phone = this.props.phone;

    return (
      <li className="thumbnail phone-listing">
        <a href="#" className="thumb"><img src={phone.imageUrl} /></a>
        <a href="#">{phone.name}</a>
        <p>{phone.snippet}</p>
      </li>
    )
  }
});

In Phone we are using the phone property to assign the image, name and snippet. Let’s worry about the links when we deal with the PhoneDetails component.

Step-5 Filter and Sort

# To reset workspace
$ git checkout -f step-5

Now that we have a dynamically generated app. Let’s add filter and sort features.

This is the time to identify the app state. Go through this article to read more about identifying the app state.

The only actions user can perform are filter and sort. So our state is

Next we need to identify where the app state should live. Go through this article read more about identifying where the state should live. In our app, we are going hold the state in PhoneCat component.

src/scripts/components/PhoneCat.js

var PhoneCat = React.createClass({
  getInitialState: function() {
      return {
        filter_text: '',
        sort_by: 'age'
      }
  },
  handleSearch: function(filter_text, sort_by) {
    this.setState({
      filter_text: filter_text,
      sort_by: sort_by
    })
  },
  render: function() {
    return (
      <div className="row">
        <div className="col-md-2">
          <SearchForm searchHandler={this.handleSearch} filter_text={this.state.filter_text} sort_by={this.state.sort_by} />
        </div>
        <div className="col-md-10">
          <PhonesList phones={this.props.phones} filter_text={this.state.filter_text} sort_by={this.state.sort_by} />
        </div>
      </div>
    )
  }
});

We need to implement the getInitialState function, to return { filter_text: '', sort_by: 'age' }, this is initial state our app should have.

Next we have added handleSearch function, it sets the state with this.setState. Whenever this.setState is executed React will automatically re-render our app with the minimal set of DOM changes required.

Then we are passing filter_text and sort_by to SearchForm and PhonesList as props. We are also passing reference to this.handleSearch as a prop to SearchForm. This is the recommended way if you need invoke a parent component method from a child component.

src/scripts/components/SearchForm.js

var SearchForm = React.createClass({
  onChangeHandler: function() {
    var query = this.refs.query.getDOMNode().value.trim();
    var order = this.refs.order.getDOMNode().value;
    this.props.searchHandler(query, order);
  },
  render: function() {
    return (
      <div>
        Search:
        <input type="text" value={this.props.filter_text} ref="query" onChange={this.onChangeHandler} />
        Sort by:
        <select value={this.props.sort_by} ref="order" onChange={this.onChangeHandler}>
          <option value="name">Alphabetical</option>
          <option value="age">Newest</option>
        </select>
      </div>
    )
  }
});

In SearchForm we are using props filter_text and sort_by set values of input and select.

For handling user events, React provides a cross-browser synthetic event system. For handling input and select change events, React provides onChange event. To read more about synthetic events go here.

onChangeHandler function uses refs to get hold of the form elements. refs is a special property, to read more go here.

getDOMNode() returns the native DOM object as you can imagine.

onChangeHandler then invokes this.props.searchHandler(query, order), which is a reference to handleSearch function from PhoneCat.

src/scripts/components/PhonesList.js

var PhonesList = React.createClass({
  render: function() {
    var props = this.props;

    var filtered = $.grep(this.props.phones, function(phone) {
      return phone.name.toLowerCase().indexOf(props.filter_text) > -1;
    });

    var sorted = filtered.sort(function(a, b) {
        if(props.sort_by === 'name')
          return a.name.localeCompare(b.name)
        else
          return a.age - b.age
      });

    var phones = sorted.map(function(phone, i){
      return <Phone phone={phone} key={i} />
    });

    return (
      <ul className="phones">
        {phones}
      </ul>
    )
  }
});

The render function of PhonesList, filters the phones property using jQuery.grep. Then sorts the filtered phones using javascript sort function.

Step-6 Ajax

# To reset workspace
$ git checkout -f step-6

In this step we will get the data from our JSON API. There is no API for the tutorial, we will just fetch /phones/phones.json from the server.

Let’s get rid of the static PHONES array.

src/scripts/components/PhoneCatWrapper.js

var PhoneCatWrapper = React.createClass({
  getInitialState: function() {
      return {
        phones: []
      }
  },
  componentDidMount: function() {
    $.getJSON('/phones/phones.json', (function(data) {
      this.setState({phones: data});
    }).bind(this));

  },
  render: function() {
    return (
      <PhoneCat phones={this.state.phones} />
    )
  }
});

We are going to store the phones data returned from the API in PhoneCatWrapper state. getInitialState function returns an empty phones array.

The componentDidMount function is invoked immediately after the component is rendered in the browser DOM. This is a good place to call our API using jQuery.getJSON function. On success we are storing the data in state using this.setState.

Step-7 Phone page

# To reset workspace
$ git checkout -f step-7

Now that we are done with the home page, it’s time to add the Phone page. Here is the screenshot of our mock page.

alt text

Here is our components hierarchy for the phone page -

- PhoneDetailsWrapper (blue)
  - PhoneDetails (green)
    - ImageGallery (red)

We will display Phone page on path /phones/some-phone-id.

Before we get into creating Phone page components, we need to resolve one issue. How is our app going to load the Phone page for path /phones/some-phone-id?

We need some routing framework for this, remember React only deals with the V of the MVC. There are many possibilities - we could use backbone routes, crossroads or many more.

We are going with react-router, it’s specific to React and will be easier to deal with.

src/scripts/main.js

require('script!react-router/dist/react-router.js')

var Route = ReactRouter.Route;
var Link = ReactRouter.Link;
var PhoneCatWrapper = require("./components/PhoneCatWrapper.js")
var PhoneDetailsWrapper = require("./components/PhoneDetailsWrapper.js")

App = React.createClass({
  render: function(){
    return(
      <div>
        <ul>
          <li><Link to="phones">Home</Link></li>
        </ul>
        <this.props.activeRouteHandler/>
      </div>
    )
  }
})

React.renderComponent(
  (<Route handler={App}>
      <Route name="phones" handler={PhoneCatWrapper}/>
      <Route name="phone" path="/phones/:phoneId" handler={PhoneDetailsWrapper} />
    </Route>), document.getElementById('app'));

Note: Home page will be accessible on - http://127.0.0.1:4000/#/phones

Here we have created an App component, that serves as routes handler. In the render function we are rendering a very basic nav-bar with only Home link. <this.props.activeRouteHandler/> renders the active route handler. You can read more about activeRouteHandler here.

Then in React.renderComponent, we are defining our routes. Route handler is App, phones route handler is PhoneCatWrapper and phone route handler is PhoneDetailsWrapper. path="/phones/:phoneId" makes the phoneId accessible to the handler via props as we will see later.

Now let’s add PhoneDetailsWrapper component, copy and paste section#main from the mock_show.html page to render function. Don’t forget to add <img> closing tags and change class to className!

src/scripts/components/PhoneDetailsWrapper.js

var PhoneDetailsWrapper = React.createClass({
  render: function() {
    return (
      <section id="main"><div className="phone-images"><img src="images/phones/motorola-xoom-with-wi-fi.3.jpg" className="phone" />
      </div>
      <h1>Motorola XOOM with Wi-Fi</h1>
      <p>Motorola XOOM with Wi-Fi has a super-powerful dual-core processor and Android 3.0 (Honeycomb)  the Android platform designed specifically for tablets. With its 10.1-inch HD widescreen display, youll enjoy HD video in a thin, light, powerful and upgradeable tablet.</p>
      <ul className="phone-thumbs"><li><img src="images/phones/motorola-xoom-with-wi-fi.0.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.1.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.2.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.3.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.4.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.5.jpg" />
      </li></ul>
      <ul className="specs"><li>
          <span>Availability and Networks</span>
          <dl>
              <dt>Availability</dt>
              <dd></dd>
          </dl>
      </li>

      <li>
          <span>Battery</span>
          <dl>
              <dt>Type</dt>
              <dd>Other ( mAH)</dd>
              <dt>Talk Time</dt>
              <dd>24 hours</dd>
              <dt>Standby time (max)</dt>
              <dd>336 hours</dd>
          </dl>
      </li>
      <!-- code deleted for brevity -->
      </ul>
      </section>
    )
  }
});

module.exports = PhoneDetailsWrapper;

src/scripts/components/Phone.js

Link = ReactRouter.Link;

var Phone = React.createClass({
  render: function() {
    var phone = this.props.phone;

    return (
      <li className="thumbnail phone-listing">
        <Link to="phone" phoneId={phone.id} className="thumb" >
          <img src={phone.imageUrl} />
        </Link>
        <Link to="phone" phoneId={phone.id} >
          {phone.name}
        </Link>
        <p>{phone.snippet}</p>
      </li>
    )
  }
});

We have modified Phone component to include Link to the Phone details page. So the links on Home page should finally be working!

Step-8 PhoneDetails and ImageGallery components

# To reset workspace
$ git checkout -f step-8

Now let’s extract PhoneDetails component from PhoneDetailsWrapper. We have simply moved the render method from PhoneDetailsWrapper to PhoneDetails.

src/scripts/components/PhoneDetailsWrapper.js

var PhoneDetails = require("./PhoneDetails.js")

var PhoneDetailsWrapper = React.createClass({
  render: function() {
    return (
      <PhoneDetails />
    )
  }
});

src/scripts/components/PhoneDetails.js

var PhoneDetails = React.createClass({
  render: function() {
    return (
      <section id="main"><div className="phone-images"><img src="images/phones/motorola-xoom-with-wi-fi.3.jpg" className="phone" />
      </div>
      <h1>Motorola XOOM with Wi-Fi</h1>
      <p>Motorola XOOM with Wi-Fi has a super-powerful dual-core processor and Android 3.0 (Honeycomb)  the Android platform designed specifically for tablets. With its 10.1-inch HD widescreen display, youll enjoy HD video in a thin, light, powerful and upgradeable tablet.</p>

      <ImageGallery />

      <ul className="specs"><li>
          <span>Availability and Networks</span>
          <dl>
              <dt>Availability</dt>
              <dd></dd>
          </dl>
      </li>

      <li>
          <span>Battery</span>
          <dl>
              <dt>Type</dt>
              <dd>Other ( mAH)</dd>
              <dt>Talk Time</dt>
              <dd>24 hours</dd>
              <dt>Standby time (max)</dt>
              <dd>336 hours</dd>
          </dl>
      </li>
      <!-- code deleted for brevity -->
      </section>
    )
  }
});

module.exports = PhoneDetails;

Let’s extract ImageGallery component from PhoneDetails.

src/scripts/components/ImageGallery.js

var ImageGallery = React.createClass({
  render: function() {
    return (
      <ul className="phone-thumbs"><li><img src="images/phones/motorola-xoom-with-wi-fi.0.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.1.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.2.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.3.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.4.jpg" />
      </li><li><img src="images/phones/motorola-xoom-with-wi-fi.5.jpg" />
      </li></ul>
    )
  }
});

module.exports = ImageGallery;

Step-9 Get phone details on Ajax

# To reset workspace
$ git checkout -f step-9

To save us time, instead of using a static PHONE array in PhoneDetailsWrapper, we are directly loading it using Ajax from the server.

src/scripts/components/PhoneDetailsWrapper.js

var PhoneDetailsWrapper = React.createClass({
  getInitialState: function() {
    return {
      phone: {
        images: [],
        battery: {},
        storage: {},
        connectivity: {},
        android: {},
        sizeAndWeight: {
          dimensions: []
        },
        display: {},
        hardware: {},
        camera: {
          features: []
        }
      }
    };
  },
  componentDidMount: function() {
    var url;
    url = "phones/" + this.props.params.phoneId + ".json";
    return $.getJSON(url, (function(data) {
      data.phone_image = data.images[0];
      return this.setState({
        phone: data
      });
    }).bind(this));
  },
  render: function() {
    return <PhoneDetails phone={this.state.phone} />
  }
});

We are saving phone details in PhoneDetailsWrapper state. getInitialState function returns the initial state required to render the component.

In componentDidMount function we are making the Ajax call using jQuery. rack-router passes url parameters to the component in this.props.params property, we are using this.props.params.phoneId to create the url for the Ajax call. On success we are setting the state with this.setState.

Finally, passing the state to PhoneDetails as phone property.

Step-10 Use props in PhoneDetails

# To reset workspace
$ git checkout -f step-10

Time to make PhoneDetails dynamic! Let’s use the phone property.

src/scripts/components/PhoneDetails.js

var PhoneDetails = React.createClass({
  checkmark: function(truthy) {
    if (truthy) {
      return String.fromCharCode(10003);
    } else {
      return String.fromCharCode(10008);
    }
  },

  render: function() {
    var phone = this.props.phone;
    var dimensions = phone.sizeAndWeight.dimensions.map(function(dimension, i) {
      return <dd key={i}>{ dimension }</dd>;
    });

    return (
      <section id="main">
        <div className="phone-images">
          <img ref="phone_image" src={ this.props.phone.images[0]} className="phone" />
        </div>
        <h1>{phone.name}</h1>
        <p>{phone.description}</p>

        <ImageGallery />

        <ul className="specs">
          <li>
              <span>Availability and Networks</span>
              <dl>
                  <dt>Availability</dt>
                  <dd>{ phone.availability }</dd>
              </dl>
          </li>
          <!-- code deleted for brevity -->
          <li>
              <span>Connectivity</span>
              <dl>
                  <dt>Network Support</dt>
                  <dd>{ phone.connectivity.cell }</dd>
                  <dt>WiFi</dt>
                  <dd>{ phone.connectivity.wifi }</dd>
                  <dt>Bluetooth</dt>
                  <dd>{ phone.connectivity.bluetooth }</dd>
                  <dt>Infrared</dt>
                  <dd>{ this.checkmark(phone.connectivity.infrared) }</dd>
                  <dt>GPS</dt>
                  <dd>{ this.checkmark(phone.connectivity.gps) }</dd>
              </dl>
          </li>
          <!-- code deleted for brevity -->
          <li>
              <span>Size and Weight</span>
              <dl>
                  <dt>Dimensions</dt>
                    { dimensions }
                  <dt>Weight</dt>
                  <dd>{ phone.sizeAndWeight.weight }</dd>
              </dl>
          </li>
          <!-- code deleted for brevity -->
      </section>
    );
  }
});

There is nothing new here, except the checkmark function. JSX has issues displaying HTML entities. There are various workarounds, we are going ahead with using String.fromCharCode. The checkmark function simply returns ✓ if the parameter truthy is true otherwise ✘.

You should see phone details dynamically populated.

Step-11 Use props in ImageGallery

# To reset workspace
$ git checkout -f step-11

Phone images are still not showing up in the gallery, let’s do something about it!

src/scripts/components/ImageGallery.js

var ImageGallery = React.createClass({
  render: function() {
    var images = this.props.images.map(function(image_path, i) {
      return <li key={i}><img src={image_path} /> </li>;
    });

    return (
      <ul className="phone-thumbs">
        {images}
      </ul>
    );
  }
});

module.exports = ImageGallery;

Nothing ground-breaking here, we are simply creating an array of images and rendering it.

Don’t forget to pass the images property to ImageGallery in PhoneDetails!

src/scripts/components/PhoneDetails.js

.
.
        <ImageGallery images={phone.images} />
.
.

Now you should see the dynamic thumbnails.

Step-12 Make thumbnail click work

# To reset workspace
$ git checkout -f step-12

Now we have everything in place, except thumbnail click is not working. Time to decide what should go in the state… the only thing user can do is click the thumbnail to change main image of the phone. We will save active_image url in the state and change it on thumbnail click.

src/scripts/components/PhoneDetails.js

var PhoneDetails = React.createClass({

  getInitialState: function() {
    return {
      active_image: ''
    }
  },
  .
  .
  .

  handleThumbClick: function(image_path) {
    this.setState( {
        active_image: image_path
      }
    )
  },

  render: function() {
    var phone = this.props.phone;
    var dimensions = phone.sizeAndWeight.dimensions.map(function(dimension, i) {
      return <dd key={i}>{ dimension }</dd>;
    });

    return (
      <section id="main">
        .
        .
        <ImageGallery images={phone.images} handleThumbClick={this.handleThumbClick} />
        .
        .
      </section>
    );
  }
});

The handleThumbClick function sets active_image state, and we are passing it’s reference to the ImageGallery as a property. So it can tell when/which thumbnail is clicked.

src/scripts/components/ImageGallery.js

var ImageGallery = React.createClass({
  handleClick: function(e) {
    return this.props.handleThumbClick(e.target.src);
  },

  render: function() {
    var images = this.props.images.map(function(image_path, i) {
      return <li key={i}><img src={image_path} /> </li>;
    });

    return (
      <ul className="phone-thumbs" onClick={this.handleClick}>
        {images}
      </ul>
    );
  }
});

In ImageGallery we wrote an onClick handler handleClick. The handleClick function, invokes this.props.handleThumbClick function which points to PhoneDetails function handleThumbClick.

Now you should see the fully functional app in action!

That’s it! We are finally done!! Please let me know your feedback/suggestions/critics in comments or mail me at girish at cuberoot dot in