@see http://sahatyalkabov.com/create-a-character-voting-app-using-react-nodejs-mongodb-and-socketio/
This is a remake of the original New Eden Faces (2013) project, which was my first ever single-page application written in Backbone.js. It has been running in production on OpenShift with Node.js 0.8.x for over 2 years now.
I usually make as few assumptions as possible about a particular topic, which is why my tutorials are so lengthy, but having said that, you need to have at least some prior experience with client-side JavaScript frameworks and Node.js to get the most out of this tutorial. Before proceeding, you will need to download and install the following tools:
Step 1. New Express Project
Create a new directory newedenfaces. Inside, create 2 empty files package.json and_server.js_ using your favorite text editor or using the command line: July 22, 2015 Update: I am using the default Terminal app in Mac OS X with Monokaitheme and oh-my-fish framework for the Fish shell. Open package.json and paste the following:
October 19, 2015 Update: Updated package versions and added two new packages:react-dom (as part of the React 0.14 changes) and history (as part of the React Router 1.0 changes). November 12, 2015 Update: Added babel
configuration presets (new in Babel 6.x). No longer uses babel-node
command to start or watch the server, instead you can use node
and nodemon
commands directly. Updated existing packages, removed unused packages, added new Babel 6.x packages. These are all the packages that we will be using in this project. Let's briefly go over each package.
Package Name | Description |
---|---|
[alt](https://github.com/goatslacker/alt) | Flux library for React. |
[async](https://github.com/caolan/async) | For managing asynchronous flow. |
[body-parser](https://github.com/expressjs/body-parser) | For parsing POST request data. |
[colors](https://github.com/marak/colors.js/) | Pretty console output messages. |
[compression](https://github.com/expressjs/compression) | Gzip compression. |
[express](http://expressjs.com/) | Web framework for Node.js. |
[history](https://github.com/rackt/history) | Manage session history in browsers, used by react-router. |
[mongoose](http://mongoosejs.com/) | MongoDB ODM with validation and schema support. |
[morgan](https://github.com/expressjs/morgan) | HTTP request logger. |
[react](http://facebook.github.io/react/) | React. |
[react-dom](https://www.npmjs.com/package/react-dom) | React rendering, it is no longer bundled with React. |
[react-router](https://github.com/rackt/react-router) | Routing library for React. |
[request](https://github.com/request/request) | For making HTTP requests to EVE Online API. |
[serve-favicon](https://github.com/expressjs/serve-favicon) | For serving _favicon.png_ icon. |
[socket.io](http://socket.io/) | To display how many users are online in real-time. |
[swig](http://paularmstrong.github.io/swig/) | To render the initial HTML template. |
[underscore](http://underscorejs.org/) | Helper JavaScript utilities. |
[xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) | For parsing XML response from EVE Online API. |
Run npm install
in the Terminal to install the packages that we specified in the_package.json_.
If you are using Windows check out cmder console emulator. It is the closest thing to Mac OS X/Linux Terminal experience.
Open server.js and paste the following code. It's a very minimal Express application, just enough to get us started.
Although we will be building the React app in ES6, I have decided to use ES5 here because this back-end code is mostly unchanged from when I built the original New Eden Faces app. Furthermore, if you are using ES6 for the first time, it won't be too overwhelming, since the Express app should still be familiar to you.
Next, create a new directory public. This is where we are going to place images,_fonts_, compiled CSS and JavaScript files. Run npm start
in the Terminal to make sure our Express app is working without any issues. You should see Express server listening on port 3000 message in the Terminal.
Step 2. Build System
If you have been around in the web community at all, then you may have heard aboutBrowserify and Webpack tools. If not, then consider what it would be like having to manually include all these <script>
tags in a specific order, because one file may depend on another file which depends on another file. Additionally, we cannot use ECMAScript 6 directly in the browsers yet. Our code needs to be transformed by Babel into ECMAScript 5 before it can be served and interpreted by the browsers. We will be using Gulp and Browserify in this tutorial instead of Webpack. I will not advocate for which tool is better or worse, but personally I found that Gulp + Browserify is more straightforward to me than an equivalent Webpack config file. I have yet to find a React boilerplate project with an easy to understand webpack.config.js file. Create a new file gulpfile.js and paste the following code:
November 12, 2015 Update: Updated babelify
transform to use es2015 and reactpresets.
If you have not used Gulp before, this is a great starting point — An Introduction to Gulp.js.
Although the code should be more or less self-explanatory with those task names and code comments, let's briefly go over each task for completeness.
Gulp Task | Description |
---|---|
`vendor` | Concatenates all JS libraries into one file. |
`browserify-vendor` | For performance reasons, NPM modules specified in the `dependencies` array are compiled and bundled separately. As a result, _bundle.js_ recompiles a few hundred milliseconds faster. |
`browserify` | Compiles and bundles just the app files, without any external modules like _react_and _react-router_. |
`browserify-watch` | Essentially the same task as above but it will also listen for changes and re-compile_bundle.js_. |
`styles` | Compiles LESS stylesheets and automatically adds browser prefixes if necessary. |
`watch` | Re-compiles LESS stylesheets on file changes. |
`default` | Runs all of the above tasks and starts watching for file changes. |
`build` | Runs all of the above tasks then exits. |
Next, we will shift focus to the project structure by creating files and folders that_gulpfile.js_ is expecting.
Step 3. Project Structure
In the public directory create 4 new folders css, js, fonts and img. Also, download this favicon.png and place it here as well. In the newedenfaces directory (project root), create a new folder app. Then inside app create 4 new folders actions, components, stores, stylesheets and 3 empty files alt.js, routes.js and main.js. In the stylesheets directory create a new file main.less which we will populate with CSS styles shortly. Back in the project root directory (newedenfaces), create a new file bower.json and paste the following:
Bower is a package manager that lets you easily download JavaScript libraries, such as the ones specified above, via a command line instead of visiting each individual website, downloading, extracting and adding it to the project manually.
Run bower install
and wait for the packages to be downloaded and installed into the bower_components directory. You can change that path using the _.bowerrc_ file, but for the purposes of this tutorial we will stick with the defaults. Similarly to node_modules, you should not commit bower_components into a Git repository. But hold on, if we don't commit it to Git, how will those files be loaded when you deploy your app? We will revisit this issue later during the deployment step of this tutorial. Copy all glyphicons fonts from bower_components/bootstrap/fonts into public/fonts directory. Download and extract the following background images and place them into public/img directory:
I have used the Gaussian blur in Adobe Photoshop in order to create that out of focus effect over 3 years ago when I built the original New Eden Faces project, but now it should be totally possible to achieve a similar effect using CSS filters.
Open main.less that we just created and paste the following styles from the link below. Due to the sheer length of it, I have decided to include it as a separate file.
If you have used the Bootstrap CSS framework in the past, then most of it should be already familiar to you. I don't know if you are aware of the latest trend to include styles directly inside React components, but I am not sure if I like this new practice. Perhaps when tooling gets better I will revisit this topic, until then I will use external stylesheets like I always have been. However, if you are interested in using modular CSS, check out css-modulesify. October 19, 2015 Update: If you are building reusable React components likeElemental UI and Material UI then by all means do it. Personally, I would actually prefer if I don't have to import accompanying "vendor" stylesheets, as we do with just much every user-interface jQuery library. Before we jump into building the React app, I have decided to dedicate the next three sections to ES6, React, Flux, otherwise it may be too overwhelming trying to learn everything at once. Personally, I had a very hard time following some React + Flux code examples written in ES6 because I was learning a new syntax, a new framework and a completely unfamiliar app architecture all at once. Since I cannot cover everything in-depth, we will be going over only those topics that you need to know for this tutorial.
Step 4. ES6 Crash Course
The best way to learn ES6 is by showing an equivalent ES5 code for every ES6 example. Again, I will only be covering what you need to know for this tutorial. There are plenty of blog posts that go in great detail about the new ES6 features. Modules (Import)
Using the ES6 destructuring assignment we can import a subset of a module which can be quite useful for modules like react-router and underscore where it exports more than one function. One thing to keep in mind is that ES6 imports are hoisted. All dependent modules will be loaded before any of the module code is executed. In other words, you can't conditionally load a module like with CommonJS. That did throw me off a little when I tried to import a module inside an if-else condition. For a detailed overview of the import
statement see this MDN page. Modules (Export)
To learn more about ES6 modules, as well as different ways of importing and exporting functions from a module, check out ECMAScript 6 modules and Understanding ES6 Modules. Classes ES6 classes are nothing more than a syntactic sugar over the existing prototype-based inheritance in JavaScript. As long as you remember that fact, the class
keyword will not seem like a foreign concept to you.
With ES6 classes you can now use extends
to create a subclass from an existing class:
October 19, 2015 Update: Added the ES5 example using React.createClass
. For more information about ES6 classes visit Classes in ECMAScript 6 blog post. var
vs let
The only difference between the two is that var
is scoped to the nearest function blockand let
is scoped to the nearest enclosing block - which could be a function, a for-loop or an if-statement block. Here is a good example showing the difference between var
and let
:
Basically, let
is block scoped, var
is function scoped. Arrow Functions (Fat Arrow) An arrow function expression has a shorter syntax compared to function expressions and lexically binds the this
value.
Parentheses around the single argument are optional, so it is up to you whether you want to enforce it or not. Some see it as a bad practice, others think it's fine.
Besides a shorter syntax, what else is it useful for? Consider the following example, straight from this project before I converted it to ES6.
Every function expression above creates its own this
scope. Without binding this
we would not be able to call this.setState
in the example above, because this
would have been undefined. Alternatively, we could have assigned this
to a variable, e.g. var self = this
and then used self.setState
instead of this.setState
inside the closures to get around this classic JavaScript problem. In any case, here is an equivalent ES6 code using fat arrow functions which preserve the original this
value:
Next, let's talk about React, what makes it so special and why should we use it.
Step 5. React Crash Course
React is a JavaScript library for building web user interfaces. You could say it competes against AngularJS, Ember.js, Backbone and Polymer despite being much smaller in scope. React is just the V in the MVC (Model-View-Controller) architecture. So, what is so special about React? React components are written in a very declarative style. Unlike the "old way" using jQuery and such, you don't interact with DOM directly. React manages all UI updates when the underlying data changes. React is also very fast thanks to the Virtual DOM and diffing algorithm under the hood. When the data changes, React calculates the minimum number of DOM manipulations needed, then efficiently re-renders the component. For example, if there are 10,000 rendered items on a page and only 1 item changes, React will update just that DOM element, leaving 9,999 other items unchanged. That's why React can get away with re-rendering the entire components without being ridiculously wasteful and slow. Other notable features of React include:
- Composability, i.e. make bigger, more complex components out of smaller components.
- Relatively easy to pick up since there isn't that much to learn and it does not have a massive documentation like AngularJS and Ember.js.
- Server-side rendering allows us to easily build Isomorphic JavaScript apps.
- The most helpful error and warning messages that I have seen in any JavaScript library.
- Components are self-contained; markup and behavior (and even styles) live in the same place, making components very reusable.
October 19, 2015 Update: We will not be building a true Isomorphic JavaScript app. If you disable JavaScript your Browser, a page will be rendered just fine for the most part, but it will not render any characters because that requires more work by fetching data from the database and then passing it to the root React component that will need to pass the data down to its children components, which is outside the scope of this tutorial. I really like this excerpt from the React v0.14 Beta 1 blog post announcement that sums up nicely what React is all about:
It's become clear that the beauty and essence of React has nothing to do with browsers or the DOM. We think the true foundations of React are simply ideas of components and elements: being able to describe what you want to render in a declarative way.
Before going any further please watch this awesome video React in 7 Minutes by John Lindquist. And while you are there, I highly recommend getting the PRO subscription ($24.99/month) to unlock over 94 React and React Native video lessons. No, you will not become an expert just by watching these videos, but they are amazing at giving you short and straight to the point explanations on any particular topic. If you are on a budget, you can subscribe for 1 month, download all the videos, then cancel your subscription at the end of the month. Subscribing not only gives you access to React lessons, but also to TypeScript, Angular 2, D3, ECMAScript 6, Node.js and more. Disclaimer: I am not affiliated with Egghead.io and I do not get any commissions for referrals. While learning React, the biggest challenge for me was that it required a completely different thinking approach to building UIs. Which is why reading Thinking in Reactguide is absolutely a must for anyone who is starting out with React. In similar fashion to the Product Table from Thinking in React, if we are to break apart the New Eden Faces UI into potential components, this is what it would look like:
Each component should try to adhere to the single responsibility principle. If you find yourself working on a component that does too many things, perhaps it's best to split it into sub-components. Having said that, I typically write monolithic components first, just to get it working, then refactor it by splitting it into smaller sub-components.
The top-level App component contains Navbar, Homepage and Footer components.Homepage component contains two Character components. So, whenever you have a certain UI design in mind, start by breaking it apart from top-down and always be mindful of how your data propagates from parent to child, child to parent and between sibling components or you will quickly find yourself completely lost. It may be difficult initially, but it will become second nature to you after building a few React apps. So, next time you decide to build a new app in React, before writing any code, do this hierarchy outline first. It will help you to visualize the relationships between multiple components and build them accordingly.
All components in React have a render()
method. It always returns a single childelement. Conversly, the following return statement is invalid because it contains 3 child elements:
The HTML markup above is actually called JSX. As far syntax goes, it is just slightly different from HTML, for example className
instead of class
to define CSS classes. You will learn more about it as we start building the app. When I first saw that syntax, I was immediately repulsed by it. I am used to returning booleans, numbers, strings, objects and functions in JavaScript, but certaintly not that. However, JSX is actually just a syntactic sugar. After fixing the code above by wrapping it with a <ul>
tag (must return a single element), here is what it looks like without JSX:
I think you will agree that JSX is far more readable than plain JavaScript. Furthermore,Babel has a built-in support for JSX, so we don't need to install anything extra. If you have ever worked with AngularJS directives then you will appreciate working with React components, so instead of having two different files — directive.js (logic) and_template.html_ (presentation), you have a single file containing both logic and presentation. The componentDidMount
method in React is the closest thing to $(document).ready
in jQuery. This method runs once (only on the client) immediately after initial rendering of the component. This is where you would typically initialize third-party libraries and jQuery plugins, or connect to Socket.IO. You will be using Ternary operator in the render
method quite a lot: hiding an element when data is empty, conditionally using CSS classes depending on some value, hiding or showing certain elements based on the component's state and etc. Consider the following example that conditionally sets the CSS class to text-danger ortext-success based on the props value.
We have only scratched the surface of everything there is to React, but this should be enough to give you a general idea about React as well as its benefits. React on its own is actually really simple and easy to grasp. However, it is when we start talking about Flux architecture, things can get a little confusing.
Step 6. Flux Architecture Crash Course
Flux is the application architecture that was developed at Facebook for building scalable client-side web applications. It complements React's components by utilizing a unidirectional data flow. Flux is more of a pattern than a framework, however, we will be using a Flux library called Alt to minimize writing the boilerplate code. Have you seen this diagram before? Did it make any sense to you? It did not make any sense to me, no matter how many times I looked at it. Now that I understand it better, I am actually really amazed by how such simple architecture can be presented in a such complicated way. But to Facebook's credit, theirnew Flux diagrams are much better than before.
When I first began writing this tutorial I decided not to use Flux in this project. I could not grasp it for the life of me, let alone teach it to others. But thankfully, I get to work on cool stuff at Yahoo where I get to play and experiment with different technologies during my work hours. Honestly, we could have built this app without Flux and it would have been less lines of code. We don't have here any complex or nested components. But I believe that showing a full-stack React app with server-side rendering and Flux architecture, to see how all pieces connect together, has a value in of itself.
Instead of reiterating the Flux Overview, let's take a look at one of the real-world use cases in order to illustrate how Flux works:
On
componentDidLoad
(when the page is rendered) three actions are fired:OverviewActions.getSummary(); OverviewActions.getApps(); OverviewActions.getCompanies();Each one of those actions makes an AJAX request to the server to fetch the data.
When the data is fetched, each action fires another "success" action and passes the data along with it:
getSummary() { request .get('/api/overview/summary') .end((err, res) => { this.actions.getSummarySuccess(res.body); }); }Meanwhile, the Overview store (a place where we keep the state for Overviewcomponent) is listening for those "success" actions. When the
getSummarySuccess
action is fired,onGetSummarySuccess
method in the Overview store is called and the store is updated:class OverviewStore { constructor() { this.bindActions(OverviewActions); this.summary = {}; this.apps = []; this.companies = []; } onGetSummarySuccess(data) { this.summary = data; } onGetAppsSuccess(data) { this.apps = data; } onGetCompaniesSuccess(data) { this.companies = data; } }As soon as the store is updated, the Overview component will know about it because it has subscribed to the Overview store. When a store is updated/changed, a component will set its own state to whatever is in that store.
class Overview extends React.Component { constructor(props) { super(props); this.state = OverviewStore.getState(); this.onChange = this.onChange.bind(this); } componentDidMount() { OverviewStore.listen(this.onChange); } onChange() { this.setState(OverviewStore.getState()) } ... }At this point the Overview component has been updated with the new data.
In screenshot above,when the date range is updated from the dropdown menu, the entire process is repeated all over again.
Action names do not matter, use whatever naming convention you want as long as it is descriptive and makes sense.
Ignoring the Dispatcher for a moment, can you see the one-way flow outlined above? If not, it's alright, it will start making more sense as we start building the app. Flux Summary Flux is really just a fancy term for pub/sub architecture, i.e. data always flows one way through the application and it is picked up along the way by various subscribers (stores) who are listening to it.
There are more than a dozen of Flux implementations at the time of this writing. Out of them all, I only have experience with RefluxJS and Alt. Between the two, I personally prefer Alt for its simplicity, great support by _@goatslacker_, server-side rendering support, great documentation and the project is actively maintained. I strongly encourage you to go through the Alt's Getting Started guide. It will take no more than 10 minutes to skim through it. If you are undecided on the Flux library, consider the following comment by glenjaminon Hacker News, in response to having a hard time figuring out which Flux library to use:
The dirty secret is: they're probably all fine. It's unlikely that you will choose a flux lib that will be the make or break point of your application. Even if a library stops being "maintained", most decent flux incarnations are really small (~100 LoC) - it's unlikely that there's some fatal flaw you'll be stuck with. In summary: redux is neat, but don't beat yourself up over choosing the perfect flux lib - just grab one that you like the look of and move on with building your application.
Now that we have covered some basics behind ES6, React and Flux, it is time to move on to building the application.
Step 7. React Routes (Client-Side)
Create a new file App.js inside app/components with the following contents:
RouteHandler
is a component that{this.props.children}
now renders the active child route handler. It will render one of the following components depending on the URL path: Home, Top 100, Profile or Add Character. October 19, 2015 Update: RouteHandler is gone. Router now automatically populatesthis.props.children
of your components based on the active route.
It is similar to <div ng-view></div>
in AngularJS, which includes the rendered template of current route into the main layout.
Next, open routes.js inside app and paste the following:
October 19, 2015 Update: The handler
prop is now called component
. Named routes are gone as well. The reason for nesting routes this particular way is because we are going to place_Navbar_ and Footer components, above and below the active route, inside the Appcomponent. Unlike other components, Navbar and Footer do not change/disappear between route transitions. (See outlined screenshot from Step 5) Lastly, we need to add a URL listener and render the application when it changes. Open_main.js_ inside the app directory that we created earlier and paste the following:
October 19, 2015 Update: React.render
now lives in the react-dom package.Router.HistoryLocation
is now handled by the history package. We use history to enable HTML5 History API and to programmatically transition between routes. Routes are now passed in to the <Router>
component as children instead of prop.
The main.js is the entry point for our React application. We use it in gulpfile.js where Browserify will traverse the entire tree of dependencies and generate the final bundle.js file. You will rarely have to touch this file after its initial setup.
React Router bootstraps the routes from routes.js file, matches them against a URL, and then executes the appropriate callback handler, which in this case means rendering a React component into <div id="app"></div>
. But how does it know which component to render? Well, for example, if we are on /
URL path, then {this.props.children}
will render the Home component, because that's what we have specified in routes.js. We will add more routes shortly. Also, notice that we are using createBrowserHistory
to enable HTML5 History API in order to make URLs look pretty. For example, it navigates to http://localhost:3000/add
instead of http://localhost:3000/#add
. Since we are building an Isomorphic React application (rendered on the server and the client) we do not have to do any hackywildcard redirects on the server to enable this support. It just works out of the box. Let's create one last React component for this section. Create a new file Home.js inside app/components with the following contents:
Below should be everything we have created up to this point. This would be a good time to double check your code. One last thing, open alt.js in the app directory and paste the following code. I will explain its purpose in Step 9 when we actually get to use it.
Now we just need to set up a few more things on the back-end and then we can finally run the app.
Step 8. React Routes (Server-Side)
Open server.js and import the following modules by adding them at the top of the file:
October 19, 2015 Update: Previous React.renderToString
now lives in thereact-dom/server
package. November 12, 2015 Update: Added Babel Require Hook. All subsequent files required by Node with the extensions .es6
, .es
, .jsx
and .js
will be transformed by Babel. Since I have switched to Require Hook, it is no longer necessary to run the app usingbabel-node
command as mentioned in Step 1. Furthermore, this Require Hook will use Babel presets we specified in package.json. To learn more about Require Hook usage and configuration, check out Babel documentation guide. Next, add the following middleware to server.js, somewhere after existing Express middlewares:
October 19, 2015 Update: Previous React.renderToString
now lives in thereact-dom/server
package. Additionally, server-side rendering with React Router has changed quite a bit. See Server Rendering Guide for more details. November 12, 2015 Update: <RoutingContext {...renderProps} />
(ES6/JSX) has been replaced with React.createElement(Router.RoutingContext, renderProps)
(ES5). That's because Babel Require Hook transforms only subsequent files, not current file. In other words, using JSX here will result in an illegal syntax error. Of course there are ways around this limitation, but I wanted to avoid creating additional files.
Note: This screenshot is now outdated as of React 0.14 and React Router 1.0 but I left it here anyway to give you a better idea of where to place this middleware in server.js.
This middleware function will be executed on every request to the server, unless a request is handled by one the API endpoints that we will implement shortly. Conditional statements within the Router.match
should be self-explanatory. Depending on if we have 500 Error, 302 Redirect, 200 Success, 404 Not Found, we take different actions. The last two — 200 Success and 404 Not Found are usually the most common responses. On the client-side, a rendered HTML markup gets inserted into <div id="app"></div>
, while on the server a rendered HTML markup is sent to the index.html template where it is inserted into <div id="app">{{ html|safe }}</div>
by the Swig template engine. I chose Swig because I wanted to try something other than Jade and Handlebars this time. But do we really need a separate template for this? Why not just render everything inside the App component? Yes, you could do it, as long as you are okay with invalid HTML markup and not being able to include inline script tags like Google Analytics directly in the App component. But having said that, invalid markup is probably not relevant to SEO anymore and there are workarounds to include inline script tags in React components. So it's up to you, but for the purposes of this tutorial we will be using a Swig template. One last thing I need to explain are those JavaScript tripple dots. It is called the ES6spread operator used in {...renderProps}
above. That's basically like saying "just pass me everything". Since renderProps
contains multiple things - routes, params,_components_, location, history, it would be a hassle to pass them as individual props. Spread operator is a handy shortcut for situations like these. October 19, 2015 Update: Added a new paragraph about the spread operator and new Router matching conditions. November 12, 2015 Update: Spread operator is no longer used, however, previous paragraph is still relevant and useful, since you will no doubt run into this ...
notation again when developing React apps. Create a new folder views in the project root directory (next to package.json and_server.js_). Then inside views, create a new file index.html:
Open two Terminal tabs. In one tab run gulp
to build the app, concatenate vendor files, compile LESS stylesheets and watch for file changes: In another tab, run npm run watch
to start the Node.js server and automatically restart the process on file changes: July 27, 2015 Update: Once again, make sure you have installed nodemon viasudo npm install -g nodemon
otherwise you will not be able to run the command above. Open http://localhost:3000 and you should see our React app render successfully: We did an impressive amount of work just to display an empty page with a simple alert message! Fortunately, the most difficult part is behind us. From here on we can relax and focus on building React components and implementing the REST API endpoints. Both gulp
and npm run watch
processes will take care of everything for us. We no longer need to worry about re-compiling the app after adding new React components or restarting the Express app after making changes to server.js.
Step 9. Footer and Navbar Components
Both Navbar and Footer are relatively simple components. The Footer component fetches and displays the Top 5 characters. The Navbar component fetches and displays the total character count and initializes a Socket.IO event listener for tracking the number of online visitors.
This section will be slightly longer than the rest since I will be covering a lot of new concepts that other sections are built upon.
Component Create a new file Footer.js inside components directory:
Just this once, I will show an ES5-equivalent code for this component in case you are still not comfortable with the new ES6 syntax. Also, see Using Alt with ES5 guide for syntax differences when creating actions and creating stores.
If you can recall the previous section on Flux architecture, then this should all be familiar to you. When component is loaded it sets the initial component state to whatever is in the FooterStore and initialzes a store listener, likewise when component is unloaded (e.g. navigated to a different page) that store listener is removed. When the store is updated, onChange
function is called, which in turn updates the Footer's state. If by any chance you have used React before, there is something you need to keep in mind when creating React components using ES6 classes. Component methods no longer autobind this
context. For example, when calling an internal component method that uses this
, you need to bind this
explicitly. Previously,React.createClass()
was doing it for us internally:
Autobinding: When creating callbacks in JavaScript, you usually need to explicitly bind a method to its instance such that the value of this is correct. With React, every method is automatically bound to its component instance.
That is why we have the following line in ES6, but not in ES5:
Here is a more complete example on this issue:
You may or may not be familiar with the map()
method in JavaScript. Even if you have used it before, it may still be unclear how it works in the context of JSX. (Something that React Tutorial regretfully does not explain very well.) It is basically a for-each loop, similar to what you might see in Jade and Handlebars, but here you can assign the results to a variable, which can then be used with JSX by wrapping it in curly braces. It's a very common pattern in React so you will be using it quite frequently.
When rendering dynamic children, such as leaderboardCharacters
above, React requires that you use the key
property to uniquely identify each child element.
A Link
component will render a fully accesible anchor tag with the proper href. It also knows when the route it links to is active and automatically applies its "active" CSS class. If you are using React Router, then you need to be using Link
for internal navigation between routes. Actions Next, we are going to create actions and a store for the Footer component. Create a new file called FooterActions.js in app/actions directory:
First, notice that we import an instance of Alt (_alt.js_ from Step 7), not the Alt module installed in node_modules. It is an instance of Alt which instantiates Flux dispatcher and provides methods for creating Alt actions and stores. You can think of it as a glue between all of our stores and actions. We have three actions here - the one that fetches the data using jQuery.ajax()
and two that notify the store whether that action was successful or unsuccessful. In this particular case, it is not very useful to know when getTopCharacters
action is fired. What we really want to know is if that action was successful (_update the store, then re-render the component with new data_) or unsuccessful (_display an error notification_). Actions can be as complex or as simple as you need them to be. Some actions are "actions" themselves, where we don't care what they do or what they send, the fact that action was fired is all we need to know. For example, ajaxInProgress
and ajaxComplete
to notify a store when AJAX request is in progress or complete.
Alt actions can be created via a shorthand notation using generateActions
method. From the documentation on Creating Actions — If all of your actions are just straight through dispatches you can shorthand generate them using this function.
The two shorthand actions above created via generateActions
and the following two simple actions are equivalent, so use either notation based on your preference:
And lastly, we wrap the FooterActions class with alt.createActions
and then export it, so that we could import and use it in the Footer component. Store Next, create a new file called FooterStore.js inside app/stores directory:
All instance variables of the store, i.e. values assigned to this
, will become part of the state. When Footer component initially calls FooterStore.getState()
it receives the current state of the store specified in the constructor (initially just an empty array, and mapping over an empty array returns another empty array, hence nothing is rendered when the Footer component is first loaded). bindActions
is a magic Alt method which binds actions to their handlers defined in the store. For example, an action with the name foo
will match an action handler method defined in the store named onFoo
or just foo
but not both. That is why for actionsgetTopCharactersSuccess
and getTopCharactersFail
defined in FooterActions.js we have corresponding store handlers called onGetTopCharactersSuccess
andonGetTopCharactersFail
in FooterStore.js.
For more precise control over which actions the store listens to and what handlers those actions are bound to, see bindListeners
method.
I hope it's pretty clear by now that when getTopCharactersSuccess
action is fired,onGetTopCharactersSuccess
handler function is executed and the store is updated with the new data that contains Top 5 Characters. And since we have initialized the store listener in the Footer component, it will be notified when the FooterStore has been updated and the component will re-render accordingly. We will be using Toastr JavaScript library for notifications. Why not just use pure React notification component you may ask? While you may find some notification components built specifically for React, I personally think it is one of the few areas that should not be handled by React (_along with tooltips_). I think it is far easier to display a notification imperatively from any part of your application than having to declaratively render notification component based on the current state. I have built a notification component with React and Flux before, but frankly it was a big pain dealing with hide/show states, animation and z-index positioning. Open App.js inside app/components and import the Footer component:
Then add <Footer />
right after the {this.props.children}
line:
Refresh the browser and you should see the new footer. We will implement Express API endpoints and populate the database with characters shortly, but for now let's continue on to the Navbar component. Since I have already covered the basics behind Alt actions and stores, and how they fit in with our app architecture, this will be a shorter sub-section.
Component Create a new file Navbar.js inside app/components directory:
October 19, 2015 Update: Removed Navbar.contextTypes
that was previously used to get an instance of the router and removed getDOMNode()
method call sincethis.refs.searchForm
already returns a DOM node now. Yes it is certainly possible to write most of the above markup dynamically with less lines of code by iterating through all races, then through all bloodlines, however, this was one of those things that I copy & pasted from my original project and didn't want to focus on too much. One thing you will probably notice right away is the class variable Now thecontextTypes
. We need it for referencing an instance of the router, which in turn gives us access to current_path_, current query parameters, route parameters and transitions to other routes.history
object (navigation) will be passed as a prop from the App component. We actually do not use it directly in the Navbar component but instead pass it as an argument to Navbar actions so that it could navigate to a particular character profile page from the Navbar store, after successfully fetching data from the server. We obviously cannot navigate from within the component since no action has been fired yet and no character data has been received. There are certainly other ways to gethistory
or router
object references inside a Flux store, but this is the least complicated solution I could think of. componentDidMount
is where we establish connection with Socket.IO and initializeajaxStart
and ajaxComplete
event listeners used for fading in/out the loading indicator on AJAX requests, which is located next to the NEF logo.handleSubmit
is a form submit handler that gets executed by pressing the Enter key or clicking the (Search) button. It essentially does some input cleanup and validation, then fires the findCharacter
action. In addition to the search query and the router instance, we also pass a reference to the search field DOM Node so that we could display a shaking animation when a character name is not found. Actions Let's create a new file NavbarActions.js in the app/actions directory:
Most of these actions should be pretty self-explanatory, but if it is unclear, see brief descriptions below.
Action | Description |
---|---|
`updateOnlineUsers` | Sets online users count on Socket.IO event update. |
`updateAjaxAnimation` | Adds "fadeIn" or "fadeOut" CSS class to the loading indicator. |
`updateSearchQuery` | Update search query value on keypress. |
`getCharacterCount` | Fetch total number of characters from the server. |
`getCharacterCountSuccess` | Returns total number of characters. |
`getCharacterCountFail` | Returns jQuery [`jqXhr`](http://api.jquery.com/jQuery.ajax/#jqXHR) object. |
`findCharacter` | Find a character by name. |
The reason why we add the shake
CSS class and then remove it one second later is so that we could repeat this animation, otherwise if we just keep on adding the shake
it will not animate again. Store Create a new file NavbarStore.js in the app/stores directory:
October 19, 2015 Update: Changed router.transitionTo
to history.pushState
for page navigation. Recall this line in the Navbar component that we created above:
Since onChange
handler returns and event object, we are using event.target.value
to get the text field value inside onUpdateSearchQuery
function. Open App.js again and import the Navbar component:
Then add <Navbar />
right before the this.props.children
component:
October 19, 2015 Update: If you recall, we created a history
object viacreateBrowserHistory
inside main.js and passed it as a prop to the <Router>
. That's why this prop is available in the App.js component. Here, we are just passing it even further down to the Navbar component. Since we haven't yet configured Socket.IO on the server or implemented any of the API routes, you will not see the total number of online visitors (_red circle next to the logo_) or total characters (_search placeholder text_).
Step 10. Socket.IO - Real-time User Count
Unlike the previous section, this one will be fairly short and focused specifically on the server-side aspect of Socket.IO. Open server.js and find the following line:
Then replace it with the following code:
In a nutshell, when a WebSocket connection is established, it increments theonlineUsers
count (global variable) and broadcasts a message — "Hey, I have this many online visitors now.". When someone closes the browser and leaves, the onlineUsers
count is decremented and it yet again broadcasts a message "Hey, someone just left, I have this many online visitors now.".
If you have never used Socket.IO then Chat application tutorial is a great starting point.
Open index.html in the views directory and add the following line right next with other scripts:
Refresh the browser and open http://localhost:3000 in multiple tabs to simulate multiple user connections. You should now see the total number of visitors in the red circle next to the logo. At this point we are neither finished with the front-end nor do we have any working API endpoints. We could have focused on building just the front-end in the first half of the tutorial and then the back-end in the second half of the tutorial, or vice versa, but personally, I have never built an app like that. I typically go back and forth between back-end and front-end parts of the application during my development flow. We can't display any characters until they are added to the database. In order to add new characters to the database we need to build a UI for it and implement an API endpoint. That's exactly what we will do next.
Step 11. Add Character Component
This component consists of a simple form with a text field, radio buttons and a submit button. Success and error messages will be displayed within help-block
under the text field. Component Create a new file AddCharacter.js in app/components directory:
You should start to see by now what all these components have in common:
- Set the initial component state to what's in the store.
- Add a store listener in
componentDidMount
, remove it incomponentWillUnmount
. - Add
onChange
method which updates component's state whenever the store is updated.
handleSubmit
does exactly what you might think — handles the form submission for adding a new character. While it is true that we could have done form validation insideaddCharacter
action instead, however, doing so would also require us to pass the text field DOM reference, because when nameTextField
is invalid, it needs to be "focused" so that a user can start typing again without having to click the text field. ** Actions** Create a new file AddCharacterActions.js in app/actions directory:
We are firing addCharacterSuccess
action when character has been added to the database successfully and addCharacterFail
when character could not be added, perhaps due to an invalid name or because it already exists in the database. Both updateName
andupdateGender
actions are fired when the Character Name text field and Gender radio button is updated via onChange
, respectively. And likewise, invalidName
andinvalidGender
actions are fired when you a user submits the form without entering a name or selecting a gender. Store Create a new file AddCharacterStore.js in app/stores directory:
nameValidationState
and genderValidationState
refers to the validation states on form controls provided by Bootstrap. helpBlock
is a status message which gets displayed below the text field, e.g. Character has been added successfully. onInvalidName
handler is fired when Character Name field is empty. If the name does not exist in EVE Online database it will be a different error message provided byonAddCharacterFail
handler. Finally, open routes.js and add a new route /add
with the AddCharacter
component handler:
Here is a quick demonstration of the entire flow from the moment you start typing a character's name:
- Fire
updateName
action, passing the event object as its payload. - Call
onUpdateName
store handler. - Update the state with the new name.
In the next few sections we will implement the back-end code for adding and saving new characters to the database.
Step 12. Database Schema
In the top-level directory (next to package.json and server.js files) create a new folder models, then inside create a new file character.js and paste the following:
A schema is just a representation of your data in MongoDB. This is where you can enforce a certain field to be of particular type. A field can also be required, unique or contain only specified characters. While a schema is just an abstract representation of the data, a model on the other hand is a more practical object with methods to query, remove, update and save data from/to MongoDB. Above, we create a Character
model and immediately export it.
Why yet another tutorial using MongoDB? Why not use MySQL, PostgreSQL, CouchDB or evenRethinkDB? That's because I don't really care enough about the database layer for the types of apps I am building. I would much rather focus on the front-end stack, because that's one of my primary interests, not databases. MongoDB may not best-suited for all use cases, but it's a decent general-purpose database and it has worked well for me in the past 3 years.
Most of these fields are pretty self-explanatory, but random
and voted
may need some context:
-
random
- an array of two numbers generated by[Math.random(), 0]
. It is a geospatialpoint as far as MongoDB is concerned. In order to grab a random character from the database we are going to use the$near
operator. I found about this "trick" fromRandom record from MongoDB on StackOverflow. -
voted
- a boolean for identifying which characters have already been voted. Previously, people were abusing the website by voting for the same character multiple times in a row. But now, when querying for two characters, only those characters that have not been voted will be fetched. Even if someone were to hit the API directly, a vote will not count for already voted characters.
Back in server.js, add the following lines at the beginning of the file, along with all other module dependencies:
Just to be consistent and systematic, I usually organize my module imports in the following order:
- Core Node.js modules — path, querystring, http.
- Third-party NPM libraries — mongoose, express, request.
- Application files — controllers, models, config.
And finally, to connect to the database, add the following code somewhere between module dependencies and Express middlewares. This will establish a connection pool with MongoDB when we start the Express app.
We will set the database hostname in config.js to avoid hard-coding the value here.
Create another file in the top-level directory called config.js and paste the following:
It will use an environment variable (if available) and fallback to "localhost". Using this approach allows us to use one hostname for local development and another hostname for production without updating any code, and it is especially useful when dealing with OAuth client keys and client secrets. Now let's import it back in server.js:
Open a new tab in Terminal and run mongod
. If you are on Windows, you will need to open mongod.exe in the directory where you installed MongoDB.
Step 13. Express API Routes (1 of 2)
In this section we will implement an Express route for fetching character information and storing it in database. We will be using EVE Online API for fetching Character ID,_Race_ and Bloodline for a given character name.
Character gender is not a public data; it requires an API key. In my opinion, what makes New Eden Faces so great is its open nature - a user does not need to be authenticated and anyone can add any other character to the roster. That is why we have two radio buttons for gender selection on the Add Characterpage. It does depend on user's honesty, however.
Below is a table that outlines each route's responsibility. However, we will not be implementing all routes, because that is something you can do on your own if necessary.
Route | POST | GET | PUT | DELETE |
---|---|---|---|---|
_/api/characters_ | Add a new character | Get random two characters | Update wins/losses for two characters | Delete all characters |
_/api/characters/:id_ | N/A | Get a character | Update a character | Delete a character |
In server.js add the following dependencies at the top:
We will use async.waterfall for managing multiple asynchronous operations andrequest module for making HTTP requests to the EVE Online API. Add our first route right after Express middlewares but before the "React middleware" that we created earlier in Step 8. React Routes (Server-Side).
I typically add block comments above my routes specifying the full path and a brief description. This allows me to quickly find the routes I am looking for using the Find...(⌘F) command as shown below.
Here is a step-by-step breakdown of how it works:
- Get a Character ID from a Character Name.
- Parse XML response.
- Query the database to check if this character is already in the database.
- Pass Character ID to the next function in the
async.waterfall
stage. - Get basic character information from a Character ID.
- Parse XML response.
- Add a new character to the database.
Go to http://localhost:3000/add then add a few characters. You could use some of the following names:
- Daishan Auergni
- CCP Falcon
- Celeste Taylor
Note: You can find additional character names over in the EVE Online Forums.
Or better yet, download this MongoDB file dump that contains over 4000 characters and import it into your database. Please ignore "duplicate key errors" if you have already added some of the characters earlier.
In Terminal, navigate to where this file has been downloaded, then run the following command to import the characters into MongoDB:
October 11, 2015 Update: Use explicit database and collection flags in the command above. You will not see updated character count in the search field just yet, since we haven't implemented an API endpoint for it. We will do that after the next section. Next, let's create the Home component - initial page that displays 2 characters side by side.
Step 14. Home Component
This is one of the simpler components whose only responsibility is to display 2 images and handle click events to know which one is the winning and which one is the losing character between the two. Component Create a new file Home.js inside components directory:
- Race: {character.race}
- Bloodline: {character.bloodline}
{character.name}
Click on the portrait. Select your favorite.
July 27, 2015 Update: Fixed the error Cannot read property 'characterId' of undefined. I have updated how the "losing" Character ID is obtained inside handleClick()
method. It uses _.findWhere
to find the "winning" character object within the array, then using_.without
we get a new array without the "winning" character. Since we only have 2 characters in the array, the other object must be the "losing" character. And finally, using _.first
we get the first (and only) object in the array. It is not really necessary to map over the characters
array since we only have 2 characters to display, but it is one way to do it. Another way would be to create a separate markup for characters[0]
and characters[1]
like so:
Click on the portrait. Select your favorite.
- Race: {characters[0].race}
- Bloodline: {characters[0].bloodline}
{characters[0].name}
- Race: {characters[1].race}
- Bloodline: {characters[1].bloodline}
{characters[1].name}
The first image is offset via col-md-offset-1
Bootstrap CSS class so both images are perfectly center-aligned. Notice we are not just binding this.handleClick
to a click event, but instead we do{this.handleClick.bind(this, character)
. Simply passing an event object is not enough, it will not give us any useful information, unlike text field, checkbox or radio button group elements. From the MSDN Documentation:
- thisArg (Required) - An object to which the
this
keyword can refer inside the new function. - arg1, arg2, ... (Optional) - A list of arguments to be passed to the new function.
To put it simply, we need to pass this
context because we are referencing this.state
inside handleClick
method, we are passing a custom object containing character information that was clicked instead of the default event object. Inside handleClick
method, the character
parameter is our winning character, because that's the character that was clicked on. Since we only have two characters it is not that hard to figure out the losing character. We then pass both winner
and loser
Character IDs to the HomeActions.vote
action. Actions Create a new file HomeActions.js inside actions directory:
We do not need voteSuccess
action here because getTwoCharacters
already does exactly what we need. In other words, after a successful vote, we need to fetch two more random characters from the database. Store Create a new file HomeStore.js inside stores directory:
Next, let's implement the remaining Express routes for fetching and updating two characters in Home component, retrieving total characters count and more. <!-- And there you have it. Refresh the browser once again and you should see two character images on the home page. Try clicking on one of them. After clicking on one of the images you should see a new set of characters appear. -->
Step 15. Express API Routes (2 of 2)
Switch back to server.js. I hope it is clear by now where you need to include all of the following routes - after Express middlewares but before the "React middleware".
Understand that we are including all routes in server.js because it is convenient to do so for the purposes of this tutorial. In the dashboard project that I had to build at work, all routes were split into separate files inside the routes directory, furthermore all route handlers were split into separate files inside the controllers directory.
Let's start with the route for fetching two characters in the Home component. GET /api/characters
Be sure to add the Underscore.js module at the top, since we are using it for _.sample()
,_.first()
and _.without()
functions:
I have tried to make this code as readable as possible, so it should be fairly easy to understand how it fetches two random characters. It will randomly select Male or_Female_ gender and query the database for two characters. If we get back less than 2 characters, it will attempt another query with the opposite gender. For example, if we have 10 male characters and 9 of them have already been voted, displaying 1 character makes no sense. If don't get back 2 characters for either Male or Female gender, that means we have exhausted all unvoted characters and the vote count should be reset by setting voted: false
for all characters.
PUT /api/characters This route is related to the previous one since it updates wins
and losses
fields of winning and losing characters respectively.
Here we are using async.parallel
to make two database queries simultaneously, since one query does not depend on another. However, because we have two separate MongoDB documents, that's two independent asynchronous operations, hence another async.parallel
. Basically, we respond with a success only when both characters have finished updating and there were no errors.
GET /api/characters/count MongoDB has a built-in count()
method for returning the number of results that match the query.
You may notice we are starting to diverge from the RESTful API design pattern with this one-off route for returning total count. Unfortunately that's just a reality. I have never worked on a project where I could perfectly map out all URLs in a RESTful way. See this post by Apigee.
GET/api/characters/search Last I checked MongoDB does not support case-insensitive queries, which explains why we have to use a regex here. The next best thing you could do is to use the $regex
operator.
GET /api/characters/top When I first built this project, I initially had around 7-9 almost identical routes for retrieving the Top 100 characters. After some code refactoring I ended up with just a single route below.
For example, if we are interested in the Top 100 male characters with Caldari race and Civire bloodline, this would be the URL path for it:
GET /api/characters/top?race=caldari&bloodline=civire&gender=male
If you are still having trouble understanding how we construct the conditions
object, this documented code should clarify it:
After we retrieve characters with the highest number of wins, we are doing another sort by winning percentage, so that we don't end up with the oldest characters always being on top.
Be careful with accepting user's input directly. Ideally we should have first checked for query params before blindly constructing the conditions
object and passing it to MongoDB.
GET /api/characters/shame Similar to the previous route, this one retrieves 100 characters with the most losses.
GET /api/characters/:id October 11, 2015 Update: I have left this Express route for last, so that other routes starting with /api/characters/, do not get clobbered by the this route with the :id
parameter. This route is used by the profile page (_Character_ component that we will build next) as shown at the beginning of the tutorial.
POST /api/report Some characters do not have a valid avatar (gray silhouette) while other avatars are nearly pitch-black and shouldn't be added to the database in the first place. But since anyone can add everyone, sometimes you end up with those characters that need be removed. A character that has been reported by visitors at least 4 times will be removed from the database.
GET /api/stats And last but not least, a route for character stats. Yes, it could be simplified withasync.each
or promises, but keep in mind when I first built New Eden Faces I was not familiar with either solutions. Most of the back-end code is unchanged since then. Although the code is verbose, at least it is explicit and very readable.
The last operation with the aggregate()
method is a bit more tricky. Admittedly, I had to get help with that part. In MongoDB, aggregations operations process data records and return computed results. In our case it computes the total number of casted votes by summing up all wins
counts. Because this is a zero-sum game, the number of wins should be exactly the same as the number of losses, so we could have used losses
counts here as well.
And we are all done here. At the end of the tutorial I will post some ideas for you to extend this project further with additional features.
Step 16. Character (Profile) Component
In this section we are going to build the profile page for a character. It is slightly different from other components primarily because of the following:
- It has a full page image background.
- Navigating from one profile page to another profile page does not unmount the component, and as a result, the
getCharacter
action insidecomponentDidMount
is never called more than once, i.e. it updates the URL but it does not fetch new data.
Component Create a new file Character.js inside app/components with the following contents:
On componentDidMount
we pass the current Character ID (from URL) to the getCharacter
action and initialize the Magnific Popup lightbox plugin.
I haven't had any success with using ref="magnificPopup"
to initialize the plugin, that's why I left it as is. This might not be the best way, but it works.
Since the Character component has a full-page background image, duringcomponentWillUnmount
it is removed from the <body>
tag so that users do not see it when navigating back to Home or Add Character components which do not have a background image. But when is this background image added? In the store when a character data is successfully fetched. One last thing that is worth mentioning again is what's happening incomponentDidUpdate
. If we are transitioning from one character page to another character page, we are still within the Character component, i.e. it is never unmounted. And if it isn't unmounted, componentDidMount
doesn't fetch new character data. So incomponentDidUpdate
— as long as we are in the same Character component and URL paths are different, e.g. transition from /characters/1807823526 to/characters/467078888, it needs to fetch new character data. Actions Create a new file CharacterActions.js inside app/actions directory:
Store Create a new file CharacterStore.js inside app/store directory:
Here we are using two Underscore's helper functions assign
and contains
to merge two objects and check if array contains a certain value, respectively.
At the time of writing Babel.js does not support Object.assign
method and I find contains
to be more readable than Array.indexOf() > -1
for checking if an array contains some value.
As I have explained before, this component looks significantly different from all other components. Adding profile
CSS class to <body>
pretty much changes the entire look and feel due to how some CSS styles are composed in main.less. While the second CSS class, which could be either caldari
, gallente
, minmatar
, amarr
(case-sensitive) determine which background image to use. I would generally avoid messing with the DOM that is not part of the render()
of that component, but this is a one-off exception. And finally, inside the onGetCharacterSuccess
handler we need to check if a character has already been reported by the same user. If they have, the report button will be grayed out and disabled. Since it is fairly easy to get around this restriction, it's probably a good idea to do an IP check on the server if you do not wish to allow your users to report a character more than once. If a character is being reported for the first time, it is saved to Local Storage under the_NEF_ namespace. Since you cannot store objects and arrays in Local Storage, we have toJSON.stringify()
it first. Again, open routes.js and a new route for /characters/:id
. This route uses a dynamic segment id
that will match any valid Character ID. Also, don't forget to import the_Character_ component.
Refresh the browser, click on one of the characters and you should see the new profile page. Up next is the CharacterList component for Top 100 characters - filtered by gender, race, bloodline and overall. The Hall of Shame is also part of this component.
Step 17. Top 100 Component
This component uses Bootstrap's Media Object as its main interface. Here is what it looks like: ** Component** Create a new file CharacterList.js inside app/components with the following contents:
{character.name}
Race: {character.race}Bloodline: {character.bloodline}
Wins: {character.wins} Losses: {character.losses}
Since our array of characters is already sorted by the winning percentage, we can useindex + 1
(1 through 100) to display the position number. It's a position only within that list, not globally across all characters. Actions Create a new file CharacterListActions.js inside app/actions directory:
The payload
, in this case, contains React Router params that we will specify in routes.jsshortly:
For example, if we go to http://localhost:3000/female/gallente/intaki, then the payload
object would contain the following data:
Store Create a new file CharacterListStore.js inside app/store directory:
Open routes.js and the following routes. All three nested routes use dynamic segments so we don't have to repeat ourselves multiple times. Make sure they are the last routes in the file, otherwise :category
can override /stats
, /add
and /shame
routes, because it will treat those routes as "categories" instead of being separate routes. Don't forget to import the CharacterList component.
September 22, 2015 Update: Fixed a bug with Hall of Shame not fetching the right characters by removing the /shame
route, since it is already passed as category
to a dynamic route below it. Here are all the valid values for dynamic segments above:
-
:category
— male, female, top. -
:race
— caldari, gallente, minmatar, amarr. -
:bloodline
— civire, deteis, achura, intaki, gallente, jin-mei, amarr, ni-kunni, khanid, brutor, sebiestor, vherokior.
As you can see, routes.js could have been much longer if we hard-coded all those routes instead of using dynamic segments.
Step 18. Stats Component
Our last component is really simple, it's just a table with general statistics such as the total number of characters by race, by gender, overall, total votes cast, leading race, leading bloodline, etc. I won't even need to explain any code because it is that simple. Component Create a new file Stats.js inside app/components directory:
Stats | |
---|---|
Leading race in Top 100 | {this.state.leadingRace.race} with {this.state.leadingRace.count} characters |
Leading bloodline in Top 100 | {this.state.leadingBloodline.bloodline} with {this.state.leadingBloodline.count} characters |
Amarr Characters | {this.state.amarrCount} |
Caldari Characters | {this.state.caldariCount} |
Gallente Characters | {this.state.gallenteCount} |
Minmatar Characters | {this.state.minmatarCount} |
Total votes cast | {this.state.totalVotes} |
Female characters | {this.state.femaleCount} |
Male characters | {this.state.maleCount} |
Total number of characters | {this.state.totalCount} |
Actions Create a new file StatsActions.js inside app/actions directory:
Store Create a new file StatsStore.js inside app/store directory:
Open routes.js and add our new route — /stats
. Again, we have to place it before the:category
route, so that it takes a higher precedence.
Refresh the browser and you should see the new Stats component:
Step 19. Deployment
Now that our project is complete we can finally deploy it. There are many hosting providers out there, but if you have followed any of my projects or tutorials then you should know why I like Heroku so much. Although deployment steps should not differ that much with other hosting providers. Let's start by creating a .gitignore file in the top-level directory of the project. You can create it either by typing touch .gitignore
in the Terminal or using your IDE / Text Editor. Add the following contents to .gitignore, where most of it is directly from the gitignorerepository on GitHub:
Remember, we are only checking in source code files to Git, not compiled CSS and JavaScript generated by Gulp.
You will also need to add the following line to package.json, inside the "scripts"
object:
Since we will not be checking in compiled CSS and JavaScript to the Git repository, or third-party libraries in bower_components, we need this postinstall
command so that Heroku could compile the app and download Bower packages after deployment, otherwise it will not have access to main.css, vendor.js, vendor.bundle.js and bundle.js files inside public directory. November 22, 2015 Update: By default, Heroku config is set to production and will install dependencies only from the dependencies
object. Inside package.json, move all packages from devDependencies
to dependencies
. Next, let's initialize a new Git repository inside newedenfaces directory:
All of our code is now checked in and we are ready to push it to Heroku. However, first we need to create a new app on Heroku. After creating a new app follow the instructions on this page: Since we already initialized a new Git repository, all you really need to do is run the following command, where newedenfaces is the name of my app, so for you it will be something else:
One last thing, click on the Settings tab, then Reveal Config Vars, then Edit button and add the following environment variable, matching what we have in config.js:
KEY | VALUE |
---|---|
`MONGO_URI` | `mongodb://admin:1234@ds061757.mongolab.com:61757/newedenfaces-tutorial` |
I have provided a sandbox database for the purposes of this tutorial, but if you wish to create your own database you can easily do so for free at MongoLab or Compose or even directly through Heroku Addons. Run the following command and we are all done!
You should now be able to see your app live at http://<appname>.herokuapp.com_.
Step 20. Additional Resources
Below is a list of resources that I found interesting and/or helpful during my own learning phase of React, Flux and ES6.
Link | Description |
---|---|
[Elemental UI](http://elemental-ui.com/) | Beautiful UI toolkit for React containing buttons, forms, spinners, modals and other components. |
[Navigating the React Ecosystem](http://www.toptal.com/react/navigating-the-react-ecosystem) | Excellent blog post by Tomas Holas exploring ES6, Generators, Babel, React, React Router, Alt, Flux, React Forms, Typeahead and Calendar widgets. In many ways it complements this tutorial. Highly recommend. |
[A Quick Tour Of ES6](http://jamesknelson.com/es6-the-bits-youll-actually-use/) | Supplemental resource for learning more about new ES6 features. Very practical and easy to read blog post. |
[Atomic CSS](http://acss.io/) | A radical new approach for styling your app. It takes time getting used to it, but when you do, its advantages are quite nice. You no longer have to abstract styles with CSS classes, instead you style React components with "atomic" classes inside your components. |
[classnames](https://github.com/JedWatson/classnames) | A JavaScript utility for conditionally joining `classNames` together. It's a more elegant solution than using ternary operator and string concatenation. |
[Iso](https://github.com/goatslacker/iso) | Helper class for Alt that allows you to pass initial data from server to client. |
In Closing
In my previous blog post that I published on December 9th, 2014 I said:
Congratulations on making it this far! It is now the longest blog post I have published to date. Funny, I said the exact same thing in my TV Show Tracker blog post.
But now, this post is even longer than my previous one. I seriously didn't expect it to be this long, neither was I trying to beat my old record. But I do hope this tutorial has been helpful and informative. If you learned at least something from this post, then all this writing effort wasn't for nothing. If you liked this project, consider extending it or perhaps build a new app based on New Eden Faces. All this code is available on GitHub and it is completely free, so use or modify it however you want. Here are some ideas for you to work on:
- Admin UI for resetting stats, swapping incorrect gender, deleting characters.
- Email subscription for weekly stats similar to Fitbit Weekly Progress Report.
- Head-to-head matches between two characters.
- Smarter matching algorithm, e.g. high winning characters matched with other high winning characters.
- List of all characters with pagination.
- Store character images on Amazon S3 or MongoDB GridFS to avoid hitting EVE Online API each time.
- Image processing algorithm to reject placeholder avatars when adding a new character.
- Automatically reset stats every X number of rounds.
- Display voting history on the character profile page.
- Archives page to view Top 100 characters from previous rounds.
- Convert API to Relay + GraphQL.
From all the emails that I have received since publishing the TV Show Tracker tutorial, I have learned that this blog attracts readers of all levels - from long-time JavaScript gurus to those who are just starting out with coding, as well as everyone in between. If you are someone who is struggling with JavaScript:
- Trust me, I have been there before. Coming from the C++ and Java background that they teach you in school, I just didn't get all that asynchronous and callbacks bullshit. At one point I got so angry and frustrated that I thought I would never use JavaScript ever again. The trick was to stop pretending like you know JavaScript and instead learn it from the ground up with an open mind.
If you are someone who is struggling with the new ES6 syntax:
- I used to loathe ES6. It did not look anything like the JavaScript I've grown to love in the past 2-3 years. Although ES6 is mostly just a syntactical sugar, it felt alien to me. Give it some time and you will grow to like it eventually. Whether someone likes it or not, that's the direction JavaScript is heading to.
If you are someone who is struggling with React:
- I remember using React for the first time and my initial thought was "What is HTML doing in my JavaScript? F that, I'll stick with AngularJS." But I don't think I need to convince you in 2015 why React is such a great library. A year ago - perhaps, but now just look at all the sites using React. React does require a new way of thinking for building apps, but once you get past that hurdle building apps in React is really fun and enjoyable. I have read a lot of React and Flux tutorials, but to be honest I did not fully understand it until I built my own project with it. I just want to reinforce that idea again - building a small project is the best way to learn any technology, not passively reading tutorials and books or watching screencasts and training videos.
If you are someone who is struggling with coding in general:
- You must learn how to persevere and deal with frustration that will no doubt arise along the way. Don't ever give up. If I gave up in 2009 I wouldn't have majored in Computer Science. If I gave up in 2012 I would've dropped out of college and never would have got my college degree. If I gave up on my Hacker School project in 2014 I would have never released Satellizer which is currently being used all over the world by thousands of developers. There will always be struggle and frustration, especially with how fast this industry is moving. Despite what you might think, I am not an expert, I still struggle just like you almost every day. It is extremely rare that I go to work and know exactly what and how needs to be done - easy breezy. If that was typically the case, then I am not advancing anywhere and probably should look for a new job.
If you are a college student seeking advice:
- Start building your portfolio right now. Go create a GitHub account and start contributing to open-source projects or build some of your own projects. Do not expect the school to teach you all the skills required from you on the job market. Don't worry too much if you have a low GPA, as long as you can compensate with a solid portfolio and open-source contributions. Companies that place too much emphasis on your GPA and school prestige are probably not the companies you want to work for, unless that's your thing. Be sure to have a goal in life and work hard towards it. Everything that I have achieved to this date is not because I am gifted and talented, or really bright, or very exceptional, or very lucky, no I am none of the above. It is because I wanted those things and I relentlessly worked hard to get it.
This is likely my last tutorial until 2016. I would like to switch back to building open-source apps and libraries so I could create more projects like Hackathon Starter andSatellizer. For questions, comments and general feedback send me an email. Also due to the high volume of emails from my previous tutorials, I am enabling comments for this post so that other readers could potentially answer some of the questions.