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:
Create the project directory structure.
mkdir -p hn/{build/js,css,html,img,js,json} cd hnNote: We will be building the project from scratch. The solution in this repo is meant primarily to be a reference.
Download the sample data into /json.
Download y18.gif and grayarrow2x.gif into /img.
Create /package.json.
{ "name": "hn", "version": "0.1.0", "private": true, "browserify": { "transform": [ ["reactify"] ] } }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
Create a new JS file: /js/NewsItem.js.
var$=require('jquery');varReact=require('react');varNewsItem=React.createClass({render: function(){return(<divclassName="newsItem"><aclassName="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.
Create a new JS file: /js/NewsItemTest.js.
var$=require('jquery');varNewsItem=require('./NewsItem');varReact=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(<NewsItemitem={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.
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; }
Create a new CSS file: /css/app.css.
body { font-family: Verdana, sans-serif; }
Create a new HTML file: /html/NewsItem.html.
<!DOCTYPE html><html><head><metacharset="utf-8"><title>NewsItem</title><linkhref="../css/NewsItem.css" rel="stylesheet"><linkhref="../css/app.css" rel="stylesheet"></head><body><divid="content"></div><scriptsrc="../build/js/NewsItemTest.js"></script></body></html>
Start Watchify. This compiles your React (JSX) components into ordinary JavaScript.
watchify -v -o build/js/NewsItemTest.js js/NewsItemTest.js
Start the HTTP server.
http-server -p 8888
Visit http://localhost:8888/html/NewsItem.html. You should see the following.


Update the JS.
// ...varurl=require('url');varNewsItem=React.createClass({// ...getDomain: function(){returnurl.parse(this.props.item.url).hostname;},render: function(){return(<divclassName="newsItem"> ... <spanclassName="newsItem-domain"> ({this.getDomain()}) </span></div>);}
Note: This code should be added onto the existing code in /js/NewsItem.js.
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.
Refresh the browser. You should see the following.

Update the JS. Note: We are factoring out the title part into its own method.
// ...varmoment=require('moment');varNewsItem=React.createClass({// ...getCommentLink: function(){varcommentText='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(<ahref={'https://news.ycombinator.com/item?id='+this.props.item.id}>{commentText}</a>);},getSubtext: function(){return(<divclassName="newsItem-subtext">{this.props.item.score} points by <ahref={'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(<divclassName="newsItem-title"> ... </div>);},render: function(){return(<divclassName="newsItem">{this.getTitle()}{this.getSubtext()}</div>);}
Update the CSS.
.newsItem-subtext { font-size:7pt; } .newsItem-subtext>a { color:#828282; text-decoration: none; } .newsItem-subtext>a:hover { text-decoration: underline; }
Refresh the browser. You should see the following.

Update the JS.
varNewsItem=React.createClass({// ...getRank: function(){return(<divclassName="newsItem-rank">{this.props.rank}. </div>);},getVote: function(){return(<divclassName="newsItem-vote"><ahref={'https://news.ycombinator.com/vote?for='+this.props.item.id+'&dir=up&whence=news'}><imgsrc="../img/grayarrow2x.gif"width="10"/></a></div>);},render: function(){return(<divclassName="newsItem">{this.getRank()}{this.getVote()}<divclassName="newsItem-itemText">{this.getTitle()}{this.getSubtext()}</div></div>);}
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; }
Refresh the browser. You should see the following.

You have now implemented an HN news item in React.

Create a new JS file: /js/NewsHeader.js.
var$=require('jquery');varReact=require('react');varNewsHeader=React.createClass({getLogo: function(){return(<divclassName="newsHeader-logo"><ahref="https://www.ycombinator.com"><imgsrc="../img/y18.gif"/></a></div>);},getTitle: function(){return(<divclassName="newsHeader-title"><aclassName="newsHeader-textLink"href="https://news.ycombinator.com">Hacker News</a></div>);},render: function(){return(<divclassName="newsHeader">{this.getLogo()}{this.getTitle()}</div>);}});module.exports=NewsHeader;
Create a new JS file: /js/NewsHeaderTest.js.
var$=require('jquery');varNewsHeader=require('./NewsHeader');varReact=require('react');React.render(<NewsHeader/>,$('#content')[0]);
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; }
Create a new HTML file: /html/NewsHeader.html.
<!DOCTYPE html><html><head><metacharset="utf-8"><title>NewsHeader</title><linkhref="../css/NewsHeader.css" rel="stylesheet"><linkhref="../css/app.css" rel="stylesheet"></head><body><divid="content"></div><scriptsrc="../build/js/NewsHeaderTest.js"></script></body></html>
Start Watchify.
watchify -v -o build/js/NewsHeaderTest.js js/NewsHeaderTest.js
Start the HTTP server if necessary.
http-server -p 8888
Visit http://localhost:8888/html/NewsHeader.html. You should see the following.

Update the JS.
// ...var_=require('lodash');varNewsHeader=React.createClass({// ...getNav: function(){varnavLinks=[{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(<divclassName="newsHeader-nav">{_(navLinks).map(function(navLink){return(<akey={navLink.url}className="newsHeader-navLink newsHeader-textLink"href={'https://news.ycombinator.com/'+navLink.url}>{navLink.name}</a>);}).value()}</div>);},render: function(){return(<divclassName="newsHeader"> ... {this.getNav()}</div>);}
Update the CSS.
.newsHeader-nav { flex-grow:1; margin-left:10px; } .newsHeader-navLink:not(:first-child)::before { content:' | '; }
Refresh the browser. You should see the following.

Update the JS.
varNewsHeader=React.createClass({// ...getLogin: function(){return(<divclassName="newsHeader-login"><aclassName="newsHeader-textLink"href="https://news.ycombinator.com/login?whence=news">login</a></div>);},render: function(){return(<divclassName="newsHeader"> ... {this.getLogin()}</div>);}
Update the CSS.
.newsHeader-login { margin-right:5px; }
Refresh the browser. You should see the following.

You have now implemented the HN header in React.

Create a new JS file: /js/NewsList.js.
var_=require('lodash');varNewsHeader=require('./NewsHeader');varNewsItem=require('./NewsItem');varReact=require('react');varNewsList=React.createClass({render: function(){return(<divclassName="newsList"><NewsHeader/><divclassName="newsList-newsItems">{_(this.props.items).map(function(item,index){return<NewsItemkey={item.id}item={item}rank={index+1}/>;}.bind(this)).value()}</div></div>);}});module.exports=NewsList;
Create a new JS file: /js/NewsListTest.js.
var$=require('jquery');varNewsList=require('./NewsList');varReact=require('react');$.ajax({url: '/json/items.json'}).then(function(items){React.render(<NewsListitems={items}/>,$('#content')[0]);});
Create a new CSS file: /css/NewsList.css.
.newsList { background:#f6f6ef; margin-left: auto; margin-right: auto; width:85%; }
Create a new HTML file: /html/NewsList.html.
<!DOCTYPE html><html><head><metacharset="utf-8"><title>NewsList</title><linkhref="../css/NewsHeader.css" rel="stylesheet"><linkhref="../css/NewsItem.css" rel="stylesheet"><linkhref="../css/NewsList.css" rel="stylesheet"><linkhref="../css/app.css" rel="stylesheet"></head><body><divid="content"></div><scriptsrc="../build/js/NewsListTest.js"></script></body></html>
Start Watchify.
watchify -v -o build/js/NewsListTest.js js/NewsListTest.js
Start the HTTP server if necessary.
http-server -p 8888
Visit http://localhost:8888/html/NewsList.html. You should see the following.

Update the JS.
varNewsList=React.createClass({// ...getMore: function(){return(<divclassName="newsList-more"><aclassName="newsList-moreLink"href="https://news.ycombinator.com/news?p=2">More</a></div>);},render: function(){return(<divclassName="newsList"> ... {this.getMore()}</div>);}
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; }
Refresh the browser. You should see the following.

You have now implemented the HN item list in React.

Create a new JS file: /js/app.js.
var_=require('lodash');var$=require('jquery');varNewsList=require('./NewsList');varReact=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 parallelvardetailDeferreds=_(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 JSONvaritems=_(arguments).map(function(argument){returnargument[0];}).value();// Render the itemsReact.render(<NewsListitems={items}/>,$('#content')[0]);});
Create a new HTML file: /html/app.html.
<!DOCTYPE html><html><head><metacharset="utf-8"><title>Hacker News</title><linkhref="../css/NewsHeader.css" rel="stylesheet"><linkhref="../css/NewsItem.css" rel="stylesheet"><linkhref="../css/NewsList.css" rel="stylesheet"><linkhref="../css/app.css" rel="stylesheet"></head><body><divid="content"></div><scriptsrc="../build/js/app.js"></script></body></html>
Start Watchify.
watchify -v -o build/js/app.js js/app.js
Start the HTTP server if necessary.
http-server -p 8888
Visit http://localhost:8888/html/app.html.
You have now implemented the HN front page in React.