Learn React by building the Hacker News front page
Switch branches/tags
Nothing to show
Clone or download
Pull request Compare This branch is 2 commits ahead of mking:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
css
html
img
js
json
sketch
.gitignore
MaintainerInfo.md
README.md
package.json

README.md

-- What is React and why do we use it?

When I have been learning new React library from Facebook, I have found this fine repo: https://github.com/mking/react-hn. I decided to add some new functionality (ability to use pagination within local site) and translate it to Russian. You can find the translation here: http://sacret.ru/react-hn-tutorial.

React HN

This is a visual React tutorial. This tutorial should give you a feel for "growing" a React UI from small, modular parts. By the end of this tutorial, you will have built the HN front page in React.

Note: This tutorial covers React, Browserify, and CSS. It does not cover event handling (not needed for the HN front page), state (not needed for the HN front page), or Flux.

This tutorial has five parts:

  1. Setup

  2. NewsItem component

  3. NewsHeader component

  4. NewsList component

  5. Display live data from the Hacker News API

  6. Additonal functionality for More link


Setup

  1. Create the project directory structure.

    mkdir -p hn/{build/js,css,html,img,js,json}
    cd hn

    Note: We will be building the project from scratch. The solution in this repo is meant primarily to be a reference.

  2. Download the sample data into /json.

  3. Download y18.gif and grayarrow2x.gif into /img.

  4. Create /package.json.

    {
      "name": "hn",
      "version": "0.1.0",
      "private": true,
      "browserify": {
        "transform": [
          ["reactify"]
        ]
      }
    }
  5. Install Browserify, React, and tools.

    # These dependencies are required for running the app.
    npm install --save react jquery lodash moment
    
    # These dependencies are required for building the app.
    npm install --save-dev browserify watchify reactify
    
    # These dependencies are globally installed command line tools.
    npm install -g browserify watchify http-server

Next


NewsItem

  1. Display the title.

  2. Add the domain.

  3. Add the subtext.

  4. Add the rank and vote.

Previous · Next


NewsItem Title

  1. Create a new JS file: /js/NewsItem.js.

    var $ = require('jquery');
    var React = require('react');
    
    var NewsItem = React.createClass({
      render: function () {
        return (
          <div className="newsItem">
            <a className="newsItem-titleLink" href={this.props.item.url}>{this.props.item.title}</a>
          </div>
        );
      }
    });
    
    module.exports = NewsItem;

    Note: You should be able to paste this code directly into your JS file.

  2. Create a new JS file: /js/NewsItemTest.js.

    var $ = require('jquery');
    var NewsItem = require('./NewsItem');
    var React = require('react');
    
    $.ajax({
      url: '/json/items.json'
    }).then(function (items) {
      // Log the data so we can inspect it in the developer console.
      console.log('items', items);
      // Use a fake rank for now.
      React.render(<NewsItem item={items[0]} rank={1}/>, $('#content')[0]);
    });

    Note: This lets us develop the NewsItem component in isolation, rather than requiring it to be hooked into the full app.

  3. Create a new CSS file: /css/NewsItem.css. We are following Jacob Thornton's CSS style guide.

    .newsItem {
      color: #828282;
      margin-top: 5px;
    }
    
    .newsItem-titleLink {
      color: black;
      font-size: 10pt;
      text-decoration: none;
    }
  4. Create a new CSS file: /css/app.css.

    body {
      font-family: Verdana, sans-serif;
    }
  5. Create a new HTML file: /html/NewsItem.html.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>NewsItem</title>
        <link href="../css/NewsItem.css" rel="stylesheet">
        <link href="../css/app.css" rel="stylesheet">
      </head>
      <body>
        <div id="content"></div>
        <script src="../build/js/NewsItemTest.js"></script>
      </body>
    </html>
  6. Start Watchify. This compiles your React (JSX) components into ordinary JavaScript.

    watchify -v -o build/js/NewsItemTest.js js/NewsItemTest.js
  7. Start the HTTP server.

    http-server -p 8888
  8. Visit http://localhost:8888/html/NewsItem.html. You should see the following.

Previous · Next

NewsItem Domain

  1. Update the JS.

    // ...
    var url = require('url');
    
    var NewsItem = React.createClass({
      // ...
    
      getDomain: function () {
        return url.parse(this.props.item.url).hostname;
      },
    
      render: function () {
        return (
          <div className="newsItem">
            ...
            <span className="newsItem-domain">
              ({this.getDomain()})
            </span>
          </div>
        );
      }

    Note: This code should be added onto the existing code in /js/NewsItem.js.

  2. Update the CSS.

    .newsItem-domain {
      font-size: 8pt;
      margin-left: 5px;
    }

    Note: This code should be added onto the existing code in /css/NewsItem.css.

  3. Refresh the browser. You should see the following.

Previous · Next


NewsItem Subtext

  1. Update the JS. Note: We are factoring out the title part into its own method.

    // ...
    var moment = require('moment');
    
    var NewsItem = React.createClass({
      // ...
    
      getCommentLink: function () {
        var commentText = 'discuss';
        if (this.props.item.kids && this.props.item.kids.length) {
          // This only counts top-level comments.
          // To get the full count, recursively get item details for this news item.
          commentText = this.props.item.kids.length + ' comments';
        }
    
        return (
          <a href={'https://news.ycombinator.com/item?id=' + this.props.item.id}>{commentText}</a>
        );
      },
    
      getSubtext: function () {
        return (
          <div className="newsItem-subtext">
            {this.props.item.score} points by <a href={'https://news.ycombinator.com/user?id=' + this.props.item.by}>{this.props.item.by}</a> {moment.utc(this.props.item.time * 1000).fromNow()} | {this.getCommentLink()}
          </div>
        );
      },
    
      getTitle: function () {
        return (
          <div className="newsItem-title">
            ...
          </div>
        );
      },
    
      render: function () {
        return (
          <div className="newsItem">
            {this.getTitle()}
            {this.getSubtext()}
          </div>
        );
      }
  2. Update the CSS.

    .newsItem-subtext {
      font-size: 7pt;
    }
    
    .newsItem-subtext > a {
      color: #828282;
      text-decoration: none;
    }
    
    .newsItem-subtext > a:hover {
      text-decoration: underline;
    }
  3. Refresh the browser. You should see the following.

Previous · Next


NewsItem Rank and Vote

  1. Update the JS.

    var NewsItem = React.createClass({
     // ...
    
     getRank: function () {
       return (
         <div className="newsItem-rank">
           {this.props.rank}.
         </div>
       );
     },
    
     getVote: function () {
       return (
         <div className="newsItem-vote">
           <a href={'https://news.ycombinator.com/vote?for=' + this.props.item.id + '&dir=up&whence=news'}>
             <img src="../img/grayarrow2x.gif" width="10"/>
           </a>
         </div>
       );
     },
    
     render: function () {
       return (
         <div className="newsItem">
           {this.getRank()}
           {this.getVote()}
           <div className="newsItem-itemText">
             {this.getTitle()}
             {this.getSubtext()}
           </div>
         </div>
       );
     }
  2. Update the CSS.

    .newsItem {
      /* ... */
      align-items: baseline;
      display: flex;  
    }
    
    .newsItem-itemText {
      flex-grow: 1;
    }
    
    .newsItem-rank {
      flex-basis: 25px;
      font-size: 10pt;
      text-align: right;
    }
    
    .newsItem-vote {
      flex-basis: 15px;
      text-align: center;
    }
  3. Refresh the browser. You should see the following.

    You have now implemented an HN news item in React.

Previous · Next


NewsHeader

  1. Display the logo and title.

  2. Add the nav links.

  3. Add the login link.

Previous · Next


NewsHeader Logo and Title

  1. Create a new JS file: /js/NewsHeader.js.

    var $ = require('jquery');
    var React = require('react');
    
    var NewsHeader = React.createClass({
      getLogo: function () {
        return (
          <div className="newsHeader-logo">
            <a href="https://www.ycombinator.com"><img src="../img/y18.gif"/></a>
          </div>
        );
      },
    
      getTitle: function () {
        return (
          <div className="newsHeader-title">
            <a className="newsHeader-textLink" href="https://news.ycombinator.com">Hacker News</a>
          </div>
        );
      },
    
      render: function () {
        return (
          <div className="newsHeader">
            {this.getLogo()}
            {this.getTitle()}
          </div>
        );
      }
    });
    
    module.exports = NewsHeader;
  2. Create a new JS file: /js/NewsHeaderTest.js.

    var $ = require('jquery');
    var NewsHeader = require('./NewsHeader');
    var React = require('react');
    
    React.render(<NewsHeader/>, $('#content')[0]);
  3. Create a new CSS file: /css/NewsHeader.css.

    .newsHeader {
      align-items: center;
      background: #ff6600;
      color: black;
      display: flex;
      font-size: 10pt;
      padding: 2px;
    }
    
    .newsHeader-logo {
      border: 1px solid white;
      flex-basis: 18px;
      height: 18px;
    }
    
    .newsHeader-textLink {
      color: black;
      text-decoration: none;
    }
    
    .newsHeader-title {
      font-weight: bold;
      margin-left: 4px;
    }
  4. Create a new HTML file: /html/NewsHeader.html.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>NewsHeader</title>
        <link href="../css/NewsHeader.css" rel="stylesheet">
        <link href="../css/app.css" rel="stylesheet">
      </head>
      <body>
        <div id="content"></div>
        <script src="../build/js/NewsHeaderTest.js"></script>
      </body>
    </html>
  5. Start Watchify.

    watchify -v -o build/js/NewsHeaderTest.js js/NewsHeaderTest.js
  6. Start the HTTP server if necessary.

    http-server -p 8888
  7. Visit http://localhost:8888/html/NewsHeader.html. You should see the following.

Previous · Next


NewsHeader Nav

  1. Update the JS.

    // ...
    var _ = require('lodash');
    
    var NewsHeader = React.createClass({
      // ...
    
      getNav: function () {
        var navLinks = [
          {
            name: 'new',
            url: 'newest'
          },
          {
            name: 'comments',
            url: 'newcomments'
          },
          {
            name: 'show',
            url: 'show'
          },
          {
            name: 'ask',
            url: 'ask'
          },
          {
            name: 'jobs',
            url: 'jobs'
          },
          {
            name: 'submit',
            url: 'submit'
          }
        ];
    
        return (
          <div className="newsHeader-nav">
            {_(navLinks).map(function (navLink) {
              return (
                <a key={navLink.url} className="newsHeader-navLink newsHeader-textLink" href={'https://news.ycombinator.com/' + navLink.url}>
                  {navLink.name}
                </a>
              );
            }).value()}
          </div>
        );
      },
    
      render: function () {
        return (
          <div className="newsHeader">
            ...
            {this.getNav()}
          </div>
        );
      }
  2. Update the CSS.

    .newsHeader-nav {
      flex-grow: 1;
      margin-left: 10px;
    }
    
    .newsHeader-navLink:not(:first-child)::before {
      content: ' | ';
    }
  3. Refresh the browser. You should see the following.

Previous · Next


NewsHeader Login

  1. Update the JS.

    var NewsHeader = React.createClass({
      // ...
    
      getLogin: function () {
        return (
          <div className="newsHeader-login">
            <a className="newsHeader-textLink" href="https://news.ycombinator.com/login?whence=news">login</a>
          </div>
        );
      },
    
      render: function () {
        return (
          <div className="newsHeader">
            ...
            {this.getLogin()}
          </div>
        );
      }
  2. Update the CSS.

    .newsHeader-login {
      margin-right: 5px;
    }
  3. Refresh the browser. You should see the following.

    You have now implemented the HN header in React.

Previous · Next


NewsList

  1. Display the header and items.

  2. Add the more link.

Previous · Next


NewsList Header and Items

  1. Create a new JS file: /js/NewsList.js.

    var _ = require('lodash');
    var NewsHeader = require('./NewsHeader');
    var NewsItem = require('./NewsItem');
    var React = require('react');
    
    var NewsList = React.createClass({
      render: function () {
        return (
          <div className="newsList">
            <NewsHeader/>
            <div className="newsList-newsItems">
              {_(this.props.items).map(function (item, index) {
                return <NewsItem key={item.id} item={item} rank={index + 1}/>;
              }.bind(this)).value()}
            </div>
          </div>
        );
      }
    });
    
    module.exports = NewsList;
  2. Create a new JS file: /js/NewsListTest.js.

    var $ = require('jquery');
    var NewsList = require('./NewsList');
    var React = require('react');
    
    $.ajax({
      url: '/json/items.json'
    }).then(function (items) {
      React.render(<NewsList items={items}/>, $('#content')[0]);
    });
  3. Create a new CSS file: /css/NewsList.css.

    .newsList {
      background: #f6f6ef;
      margin-left: auto;
      margin-right: auto;
      width: 85%;
    }
  4. Create a new HTML file: /html/NewsList.html.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>NewsList</title>
        <link href="../css/NewsHeader.css" rel="stylesheet">
        <link href="../css/NewsItem.css" rel="stylesheet">
        <link href="../css/NewsList.css" rel="stylesheet">
        <link href="../css/app.css" rel="stylesheet">
      </head>
      <body>
        <div id="content"></div>
        <script src="../build/js/NewsListTest.js"></script>
      </body>
    </html>
  5. Start Watchify.

    watchify -v -o build/js/NewsListTest.js js/NewsListTest.js
  6. Start the HTTP server if necessary.

    http-server -p 8888
  7. Visit http://localhost:8888/html/NewsList.html. You should see the following.

Previous · Next


NewsList More

  1. Update the JS.

    var NewsList = React.createClass({
      // ...
    
      getMore: function () {
        return (
          <div className="newsList-more">
            <a className="newsList-moreLink" href="https://news.ycombinator.com/news?p=2">More</a>
          </div>
        );
      },
    
      render: function () {
        return (
          <div className="newsList">
            ...
            {this.getMore()}
          </div>
        );
      }
  2. Update the CSS.

    .newsList-more {
      margin-left: 40px; /* matches NewsItem rank and vote */
      margin-top: 10px;
      padding-bottom: 10px;
    }
    
    .newsList-moreLink {
      color: black;
      font-size: 10pt;
      text-decoration: none;
    }
  3. Refresh the browser. You should see the following.

    You have now implemented the HN item list in React.

Previous · Next


Hacker News API

  1. Create a new JS file: /js/app.js.

    var _ = require('lodash');
    var $ = require('jquery');
    var NewsList = require('./NewsList');
    var React = require('react');
    
    // Get the top item ids
    $.ajax({
      url: 'https://hacker-news.firebaseio.com/v0/topstories.json',
      dataType: 'json'
    }).then(function (stories) {
      // Get the item details in parallel
      var detailDeferreds = _(stories.slice(0, 30)).map(function (itemId) {
        return $.ajax({
          url: 'https://hacker-news.firebaseio.com/v0/item/' + itemId + '.json',
          dataType: 'json'
        });
      }).value();
      return $.when.apply($, detailDeferreds);
    }).then(function () {
      // Extract the response JSON
      var items = _(arguments).map(function (argument) {
        return argument[0];
      }).value();
    
      // Render the items
      React.render(<NewsList items={items}/>, $('#content')[0]);
    });
    
  2. Create a new HTML file: /html/app.html.

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <title>Hacker News</title>
        <link href="../css/NewsHeader.css" rel="stylesheet">
        <link href="../css/NewsItem.css" rel="stylesheet">
        <link href="../css/NewsList.css" rel="stylesheet">
        <link href="../css/app.css" rel="stylesheet">
      </head>
      <body>
        <div id="content"></div>
        <script src="../build/js/app.js"></script>
      </body>
    </html>
  3. Start Watchify.

    watchify -v -o build/js/app.js js/app.js
  4. Start the HTTP server if necessary.

    http-server -p 8888
  5. Visit http://localhost:8888/html/app.html.

Previous


NewsList More Additions

  1. Update the NewsList.js file.

    var NewsList = React.createClass({
      // ...
    
      statics: {
        getQueryVariable: function getQueryVariable(variable) {
          var query = window.location.search.substring(1);
          var vars = query.split("&");
          for (var i = 0; i < vars.length; i++) {
            var pair = vars[i].split("=");
            if (pair[0] == variable) {
              return pair[1];
            }
          }
          return false;
        },
      },
      
      getMore: function () {
        var page = 2;
        if (NewsList.getQueryVariable('p')) {
          page = parseInt(NewsList.getQueryVariable('p')) + 1;
        }
        return (
          <div className="newsList-more">
            <a className="newsList-moreLink" href={"http://localhost:8888/html/app.html?p=" + page}>More</a>
          </div>
        );
      },
    
      render: function () {
        var page = 0;
        if (parseInt(NewsList.getQueryVariable('p'))) {
          page = parseInt(NewsList.getQueryVariable('p')) - 1;
        }
        return (
          <div className="newsList">
            <NewsHeader/>
            <div className="newsList-newsItems">
              {_(this.props.items).map(function (item, index) {
                var rank = index + 1 + page * 30;
                return <NewsItem key={item.id} item={item} rank={rank}/>;
              }.bind(this)).value()}
            </div>
            {this.getMore()}
          </div>
        );
      }
  2. Update the app.js file.

    // ...
    
    $.ajax({
      url: 'https://hacker-news.firebaseio.com/v0/topstories.json',
      dataType: 'json'
    }).then(function (stories) {
      // Get the item details in parallel
      var currPage = parseInt(NewsList.getQueryVariable('p'));
      var start = 0, end = 30;
      if (currPage) {
        start = (currPage - 1) * 30;
        end = currPage * 30;
      }
      var detailDeferreds = _(stories.slice(start, end)).map(function (itemId) {
        return $.ajax({
          url: 'https://hacker-news.firebaseio.com/v0/item/' + itemId + '.json',
          dataType: 'json'
        });
      }).value();
      return $.when.apply($, detailDeferreds);
    })
    
    // ...
  3. Start Watchify.

    watchify -v -o build/js/app.js js/app.js
  4. Start the HTTP server if necessary.

    http-server -p 8888
  5. Visit http://localhost:8888/html/app.html.

    You have now implemented the HN front page in React.

Previous