[转] Demistifying Modern Javascript Tools

The goal

ReactJS has become very popular recently, the community is growing fast and more and more sites use it, so it seems like something worth learning. That’s why I decided to explore it.

There is so many resources and so many examples over the internet that it’s difficult to wrap your head around it all, especially when you are new to the modern frontend stack.
There are examples with ES6 syntax, without ES6 syntax, with old react-router syntax, with new react-router syntax, examples with universal apps, with non-universal apps, with Grunt, with Gulp, with Browserify, with Webpack, and so on. I was confused with all of that. It was hard to establish what is the minimal toolset needed to achieve my goal.
And the goal was: to create a universal application with development and production environments (with minified assets in production).

This post is the first of the series describing my journey while learning modern Javascript tools. It has the form of a tutorial how to create a universal app using bare react, then Flux and lastly Redux.

Why universal? What does it mean? Do I need this?

The easiest way to create a ReactJS app is just to have an index.html file with ReactJS library included as regular Javascript file.

 
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello React!</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.1/react-dom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
  </head>
  <body>
    <div id="example"></div>
    <script type="text/babel">
      ReactDOM.render(
        <h1>Hello, world!</h1>,
        document.getElementById('example')
      );
    </script>
  </body>
</html>

 

It seems easy, so why have I seen example applications which have their own frontend servers? I started wondering why would I even need the server if I can just have a simple HTML file.

And the answer is: sure, you can create a modern dynamic application just by using simple HTML file, but you need to keep in mind that it’s content will be rendered on the client side only.
It means that if you view the page source in the browser, or make a curl request to your site, all you will see is your main div where the app is injected, but the div itself will be empty.

If the above doesn’t convince you, then perhaps this will: Google bots won’t see the content of your app if it’s only rendered on the client side. So if you care about SEO, you should definitely go with a universal app – an app which is not only rendered dynamically on the client side but also on the server side.
To achieve this you need a separate server for frontend.

You can see people referring to these kinds of apps as isomorphic. Universal is just a new, better name.

Modern Javascript tools

My goal was to create a separate frontend app with following characteristics:

  1. Server side Javascript rendering. So we need a server for this.
  2. JS scripts written in EcmaScript6 syntax. So we need something to transpile ES6 to ES5 (ES6 is not fully supported in browsers yet).
  3. Stylesheets written in Sass. So we need something to transpile SASS into CSS.
  4. All Javascript bundled in one file and all stylesheets bundled in another file. So we need a file bundler of some sort.
  5. Assets (js, css) minified for production.
  6. A mechanism to watch for changes and transpile on the fly in development mode, to speed up work flow.
  7. Something to handle external dependencies.

After looking at many examples on the Internet, my mind looked like this:

JS words cloud

I didn’t know what all of these tools do exactly and which of them I needed. E.g. do I need “browser-side require() the Node.js way” if I already decided to use ES6? Do I need Bower if I already have npm? Do I need Gulp at all?

After lots of reading I finally managed to group the tools:

words cloud grouped

EcmaScript6 (ES6)

ES6 is the new Javascript syntax, standardised in 2014. Although it’s not implemented in all browsers yet, you can already use it. What you need it to somehow transform it to currently implemented Javascript standard (ES5). If you are familiar with CoffeeScript, it’s the same process – you write using one syntax and use a tool, e.g. Babel to translate it to another. This process has a fancy name – transpilation.

As ES6 introduces lots of convenient features which will soon be implemented in browsers, in my opinion there is no need to use CoffeeScript right now. That’s why I choose to use ES6.

Module definitions

One of many convenient features of ES6 is the ability to define modules in a convenient and universal way.

Javascript didn’t have any native mechanism capable of managing dependencies before. For a long time the workaround for this was using a mix of anonymous functions and the global namespace:

 

Unfortunately, it didn’t specify dependencies between files. The developer was responsible for establishing the correct order of included files by hand.
As you can suspect, it was very error prone.

CommonJS

That’s why the CommonJS committee was created with a goal to create a standard for requiring modules.

It was implemented in Node.js. Unfortunately, this standard works synchronously. Theoretically it means that it’s not well adapted to in-browser use, given that the dynamic loading of the Javascript file itself has to be asynchronous.

AMD

To solve this problem, a next standard was proposed – Asynchronous Module Definition (AMD).
It has some disadvantages, though. Loading time depends on latency, so loading dependencies can take long too.
Incoming HTTP/2 standard is meant to drastically reduce overhead and latency for each single request, but until that happens, some people still prefer the CommonJS synchronous approach.

While setting up Babel you can choose which module definition standard you want to have in the transpiled output. The default is CommonJS.

So when you define you module in new ES6 syntax:

 
view raw es6.js hosted with  by GitHub
// calculator.js
export function sum(a, b) {
  return a + b;
};

// app.js
import calculator from "calculator";

console.log(calculator.sum(1, 2));

 

It will be translated to the chosen standard.

If you’ve chosen CommonJS the above module would be transpiled to:

 
view raw comon.js hosted with  by GitHub
// calculator.js
module.exports.sum = function (a, b) {
  return a + b;
};

// app.js
var calculator = require("calculator");

console.log(calculator.sum(1, 2));

 

And for AMD:

 
view raw amd.js hosted with  by GitHub
// calculator.js
define({
  sum: function(a, b) {
    return a + b;
  }
});

// app.js
define(["./calculator"], function(calculator) {
  console.log(calculator.sum(1, 2));
});

 

Module loaders

Having standards for defining modules is one thing, but the ability to use it in the Javascript environment is another.
To make it work in the environment of your choice (browser, Node.js etc.) you need to use a module loader. So module loader is a thing that loads your module definition in the environment.
There are many available options you can choose from: RequireJS, Almond (minimalistic version of RequireJS), Browserify, Webpack, jspm, SystemJs.

You just need to choose one and follow the documentation on how to define your modules.
For example, RequireJS supports the AMD standard, Browserify by default CommonJS, Webpack and jspm support both AMD and CommonJS, and SystemJS supports CommonJS, AMD, System.register and UMD.

Dependencies

Your app usually depends on some libraries. You could just download and include all of them in your files, but it’s not very convenient and quickly gets out of hand in larger projects.
There are a few tools for dependency management. If you use Node.js, you are probably familiar with it’s package manager – npm.
Another very popular one is Bower.

Since I needed to use Node.js to implement the frontend server, I decided to go with npm.

Shimming

In npm, all libraries are exported in the same format. But, of course, it can happen that the library you want to use is not available via npm, but only via Bower.

In such chase remember that some of the libraries may be exported in a different format than what you’re using in your application (e.g. as globals).
In order to use those libraries, you need to wrap them in some kind of adapting abstraction. This abstraction is called a shim.
Please check your module loader documentation how to do shimming.

Task runners

If you use npm you can define simple tasks in your top-level package.json file.

It’s convenient as a starting point, but if your app grows it may not be sufficient anymore. If you need to specify many tasks with dependencies between them, I recommend one of popular task runners such as Gulp or Grunt.

Template engines

Template engines are useful if you need to have dynamically generated HTML. They enable you to use Javascript code in HTML.
If you are familiar with erb you can use ejs. If you prefer haml, you would probably like Jade.

Server

Last but not least I need a server. Node.js has a built-in one, but there is also Express.
Is Express better? What is the difference? Well, with Express you can define routing easily:

 
view raw express.js hosted with  by GitHub
var express = require('express');
var app = express();
 
app.get('/', function (req, res) {
  res.send('Hello World');
});
 
app.listen(3000);

 

It looks really good, but I’ve also seen many examples using routing specific to ReactJS – implemented with react-router.
I wanted to use react-router too, as it seems more ‘ReactJS way’. Fortunately there is a way to combine react-router with Express server by using match method from react-router.

Choices

Summing up, here are my choices matched with characteristics that I defined at the begging of this post:

  1. Server side Javascript rendering – Express as the frontend server
  2. JS scripts written in EcmaScript6 syntax – transpiling ES6 to ES5 using Babel loaded through Webpack
  3. Stylesheets written in Sass – transpiling SASS into CSS using sass-loader for Webpack
  4. All Javascript bundled in one file and all stylesheets bundled in another file – Webpack
  5. Assets (js, css) minified for production – Webpack
  6. A mechanism to watch for changes and transpile on the fly in development mode, to speed up work flow – Webpack
  7. Something to handle external dependencies – npm

Additionally I chose Ejs for the layout template and since I’m using npm and Webpack we don’t really need to bother with grunt or Gulp task runners.

But of course, you can choose differently since there is a lot of other combinations:

choices

Now that we know what we want to use, in the next post we will move on to creating the app. See you next week!

 Update: Here is the next post.

Previously in the adventures series

In the last post we decided to use following tools:

  1. Server side Javascript rendering – Express as the frontend server
  2. JS written in EcmaScript6 syntax – transpiling ES6 to ES5 using Babel loaded through Webpack
  3. Stylesheets written in Sass – transpiling SASS into CSS using sass-loader for Webpack
  4. All Javascript bundled in one file and all stylesheets bundled in another file – Webpack
  5. To minify assets (js, css) for production – Webpack
  6. A mechanism to watch for changes and transpile on the fly in development mode, to speed up workflow – Webpack
  7. Something to handle external dependencies – npm

Now we’ll learn how to set them up.

Idea

We will be creating a simple application for rating submissions. This is a really simplified version of the application we used for evaluating submissions for a RailsGirls event.

We need a form for creating new submissions:

submission-form

We will display pending, evaluated and rejected submissions in separate but similar listings. All listings will have “first name” and “last name” columns, evaluated submissions will additionally have a “mark” column and rejected will have a “reason” column.

evaluated

The last view that we need is the detailed submission view with the rating.

submistion-details

Dependencies

Firstly, let’s create package.json with the application dependencies:

 
{
  "version": "0.0.1",
  "private": true,
  "name": "oceny-frontend",
  "dependencies": {
    "axios": "0.7.0",
    "ejs": "2.3.4",
    "express": "4.13.3",
    "history": "1.12.3",
    "nodemon": "1.8.0",
    "react": "0.14.2",
    "react-dom": "0.14.0",
    "react-router": "1.0.0-rc2"
  },
  "scripts": {
    "start": "nodemon --exec babel-node -- server.js",
    "build": "rimraf dist && NODE_ENV=production webpack --config ./webpack/production.config.js --progress --profile --colors",
    "production": "NODE_ENV=production npm start"
  },
  "devDependencies": {
    "babel-cli": "6.3.13",
    "babel-core": "6.3.13",
    "babel-preset-es2015": "6.3.13",
    "babel-plugin-syntax-jsx": "6.3.13",
    "babel-preset-react": "6.3.13",
    "babel-preset-stage-0": "6.3.13",
    "babel-loader": "6.2.0",
    "css-loader": "0.19.0",
    "extract-text-webpack-plugin": "0.8.2",
    "html-webpack-plugin": "1.6.1",
    "node-jsx": "0.13.3",
    "node-sass": "3.3.3",
    "node-sass-middleware": "0.9.6",
    "rimraf": "2.4.3",
    "sass-loader": "3.0.0",
    "style-loader": "0.12.4",
    "webpack": "1.12.2",
    "webpack-dev-middleware": "1.2.0"
  }
}

 

Take a look at the ‘scripts’ key, it’s where we define the application tasks:

 
  "scripts": {
    "start": "nodemon --exec babel-node -- server.js",
    "build": "rimraf dist && NODE_ENV=production webpack --config ./webpack/production.config.js --progress --profile --colors",
    "production": "NODE_ENV=production npm start"
  }

 

  • babel-node – to be able to write server.js file in ES6
  • start – for starting the server in development mode
  • build – for building production assets
  • production – for starting the server in production mode

To install the specified dependencies run npm install from the console, in the project directory.

To start the server execute npm start.

To run in production mode execute npm run build first and then npm run production.

Server

As you can see, we are running the server by executing server.js. We need to create it then:

 
import React from 'react';
import { match, RoutingContext } from 'react-router';
import ReactDOMServer from 'react-dom/server';
import Express from 'express';
import http from 'http';
import Routes from './src/routes';
import Webpack from 'webpack';
import WebpackMiddleware from 'webpack-dev-middleware';
import DefaultConfig from './webpack/default.config.js';
import DevConfig from './webpack/development.config.js';

let app = Express();
let port = process.env.PORT || DefaultConfig.Port;
const isDevelopment = process.env.NODE_ENV !== 'production';
const isProduction = process.env.NODE_ENV === 'production';

app.engine('ejs', require('ejs').__express);
app.set('view engine', 'ejs');
app.use(Express.static(DefaultConfig.Dist));

if (isDevelopment) {
  const compiler = Webpack(DevConfig);
  app.use(WebpackMiddleware(compiler, {
    publicPath: DevConfig.output.publicPath,
    noInfo: true
  }));
}

if (isProduction) {
  app.set('views', DefaultConfig.Dist);
}

app.use((req, res) => {
  match({ routes: Routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      res.render('index', {
        isDevelopment: isDevelopment,
        app: ReactDOMServer.renderToString(<RoutingContext {...renderProps} />)
      });
    } else {
      res.status(404).send('Not found');
    }
  })
});

http.createServer(app).listen(port, function() {
  console.log('Express server listening on port ' + port);
});

 

Now let’s understand what different parts of this code do. This line creates an Express application:

 
let app = Express();

 

Which we’ll configure later:

 
// ejs templates configuration
app.engine('ejs', require('ejs').__express);
app.set('view engine', 'ejs');

// static files served from dir configured in DefaultConfig.Dir
app.use(Express.static(DefaultConfig.Dist));

// in development use WebpackMiddleware to make js/css bundle
if (isDevelopment) {
  const compiler = Webpack(DevConfig);
  app.use(WebpackMiddleware(compiler, {
    publicPath: DevConfig.output.publicPath,
    noInfo: true
  }));
}

// build for production is in ./dist directory
if (isProduction) {
  app.set('views', DefaultConfig.Dist);
}

// routing setup
app.use((req, res) => {
  match({ routes: Routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.status(500).send(error.message);
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (renderProps) {
      res.render('index', {
        isDevelopment: isDevelopment,
        app: ReactDOMServer.renderToString(<RoutingContext {...renderProps} />)
      });
    } else {
      res.status(404).send('Not found');
    }
  })
});

 

And then we start the actual server:

 
http.createServer(app).listen(port, function() {
  console.log('Express server listening on port ' + port);
});

 

Index

By default Express looks for the view to render in the views directory, so let’s create our index.ejs there:

 
view raw index.ejs hosted with  by GitHub
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello Tab</title>
    <% if (isDevelopment) { %>
      <link href="/bundle.css" rel="stylesheet" type="text/css" />
    <% } %>
  </head>
  <body>
    <div id="app"><%- app %></div>
    <% if (isDevelopment) { %>
      <script src="/bundle.js" type="text/javascript"></script>
    <% } %>
  </body>
</html>

 

There are two important things going on here. Firstly, this is the div where all of our app will be injected:

 
<div id="app"><%- app %></div>

 

Secondly, we attach bundle.js (and bundle.css) only in development:

 
<% if (isDevelopment) { %>
  <script src="/bundle.js" type="text/javascript"></script>
<% } %>

 

It’s important to do it only for development because in production we’ll have our assets minified with fingerprints (e.g. bundle-9bd396dbffaafe40f751.min.js). We’ll use the Webpack plugin to inject javascript and stylesheet bundles for production.

Webpack

Development config

We included bundle.js, but we don’t have it yet, so let’s configure Webpack. Create a webpack directory and inside add the file development.config.js:

  • entry – defines entry points, the places from which Webpack starts bundling your application bundles (see the actual value in the shared config below – two entry points, one for stylesheets, one for javascript)
  • output – defines where the output file will be saved, how it will be named and how you can access it from the browser
  • module – defines loaders (for transpiling ES6, sass, etc.)
  • plugins – defines plugins (e.g. we use ExtractTextPlugin to extract the stylesheets to a separate output file)

Some parts will be shared between development and production, so I extracted them to default.config.js:

 
var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  Port: 3000,
  BundleName: 'bundle',
  Dist: path.join(__dirname, '..', 'dist'),
  Entries: [
    path.join(__dirname, '..', 'src', 'application.js'),
    path.join(__dirname, '..', 'css', 'application.scss')
  ],
  Loaders: [
    {
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loaders: ["babel-loader"]
    },
    {
      test: /\.css$/,
      loader: ExtractTextPlugin.extract("style-loader", "css-loader")
    }, {
      test: /\.scss$/,
      loader: ExtractTextPlugin.extract("style-loader", "css-loader!sass-loader")
    }
  ]
};

 

As you can see, here we configure:

  • how our bundle will be named,
  • on which port our server will start,
  • where our static assets will be served from (we use it in server.js),
  • entries which are the starting points for bundling,
  • loaders which we want to use:
    • babel-loader for ES6,
    • css-loader for ExtractTextPlugin
    • sass-loader for Sass

Production config

As I mentioned, for production we want assets to be minified and attached in HTML with fingerprints. That’s why we need a separate config:

 
var path = require('path');
var Webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var DefaultConfig = require('./default.config.js');

module.exports = {
  devtool: 'source-map',
  entry: DefaultConfig.Entries,
  output: {
    path: DefaultConfig.Dist,
    publicPath: '/',
    filename: DefaultConfig.BundleName + '-[hash].min.js'
  },
  module: {
    loaders: DefaultConfig.Loaders
  },
  plugins: [
    new Webpack.optimize.OccurenceOrderPlugin(),
    new Webpack.optimize.UglifyJsPlugin({
      compressor: {
        warnings: false,
        screw_ie8: true
      }
    }),
    new Webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
    }),
    new ExtractTextPlugin(DefaultConfig.BundleName + '-[hash].min.css'),
    new HtmlWebpackPlugin({
      template: path.join(__dirname, '..', 'views', 'index.ejs'),
      inject: 'body',
      filename: 'index.ejs'
    })
  ]
};

 

Entry points

We specified entry points to our application as: src/application.js, css/application.scss – but we don’t have them yet. Let’s add them!

Create application.scss in the css directory:

 
@import "normalize";
@import "main";

 

Also download these two css files and save them in the css directory: main.scss, normalize.css. Then create an application.js file in the src directory:

 
import React from 'react';
import ReactDOM from 'react-dom';
import Router from 'react-router';
import Routes from './routes'

import createHistory from 'history/lib/createBrowserHistory'
let history = createHistory()

var app = document.getElementById('app');

ReactDOM.render(<Router history={history} routes={Routes} />, app);

 

This file is the entry point for our client side application. Notice the render method – it’s responsible for injecting your component tree into the specified element. For us, it’s the div with “app” id.

Routes

In application.js we imported the routes.js file that we don’t have yet.

Let’s create only two routes for now:

 
import React from 'react';
import { Route } from 'react-router';
import Main from './components/Main';
import PendingSubmissionsPage from './components/PendingSubmissionsPage';
import SubmissionFormPage from './components/SubmissionFormPage';

export default (
  <Route path="/" component={Main}>
    <Route path="pending" component={PendingSubmissionsPage} />
    <Route path="submissions/new" component={SubmissionFormPage} />
  </Route>
);

 

This means that when we go to /submissions/new, the SubmissionFormPage component will be rendered. But notice that the route is nested in the / route, which is assigned to the Main component.

It’s because we want Main to be some kind of layout component, with the menu, which will be visible all the time.

And all its child routes will be rendered inside the Main component thanks to the this.props.children directive:

 
import React from 'react';
import { Link } from 'react-router'

class Main extends React.Component {
  render() {
    return (
      <div>
        <ul className="menu">
          <li><Link to="/pending">Pending</Link></li>
          <li><Link to="/evaluated">Evaluated</Link></li>
          <li><Link to="/rejected">Rejected</Link></li>
          <li><Link to="/submissions/new">Submission form</Link></li>
        </ul>
        <div className="clear">
          {this.props.children}
        </div>
      </div>
    )
  }
};

export default Main;

 

And in SubmissionFormPage we would have the actual form:

 
import React from 'react';
import ReactDOM from 'react-dom';
import Connection from '../lib/Connection';

class SubmissionForm extends React.Component {
  handleSubmit(e) {
    e.preventDefault();
    const firstName = this.refs.firstName.value.trim();
    const lastName = this.refs.lastName.value.trim();
    this.refs.firstName.value = '';
    this.refs.lastName.value = '';

    const data = {
      submission: {
        first_name: firstName,
        last_name: lastName
      }
    };
    Connection.post('/submissions', data).then(() => {
      console.log(data);
    });
  }

  render() {
    return (
      <form className="submission-form" onSubmit={this.handleSubmit.bind(this)}>
        <div className="form-field">
          <div className="label">First name:</div>
          <input name="Name" ref="firstName" />
        </div>
        <div className="form-field">
          <div className="label">Last name:</div>
          <input name="Lastname" ref="lastName" />
        </div>
        <div className="form-field">
          <input type="submit" value="Submit" />
        </div>
      </form>
    )
  }
};

export default SubmissionForm;

 

Create the above components in src/components directory. As you can see, each ReactJS component has a render method which defines the HTML to be rendered. It’s not pure HTML, it’s HTML in Jsx syntax, to make it easy to write HTML in Javascript code.  

Connection to API

In the above file, you could also notice that when submitting the form we make a request to the backend API. We will use Axios to do this. Let’s create src/lib/Connection.js:

 
import Axios from 'axios';

const BASE_URL = 'http://localhost:3001/api';

class Connection {
  get(path) {
    return Axios.get(`${BASE_URL}${path}`)
  }

  post(path, data) {
    return Axios.post(`${BASE_URL}${path}`, data)
  }
}

const connection = new Connection();

export default connection;

 

Displaying submissions

To check if everything works, it would be convenient to be able to see the pending submissions list, so let’s create PendingSubmissionsPage:

 
import React from 'react';
import SubmissionsList from './SubmissionsList';
import Connection from '../lib/Connection';

class PendingSubmissionsPage extends SubmissionsList {
  constructor(props) {
    super(props);
    this.state = {
      submissions: []
    };
  }

  componentDidMount() {
    Connection.get('/submissions/pending').then((response) => {
      this.setState({ submissions: response.data });
    });
  }

  render() {
    return (
      <SubmissionsList attributes={['first_name', 'last_name']}
        submissions={this.state.submissions} />
    );
  }
};

export default PendingSubmissionsPage;

 

As you can see here, in componentDidMount we load submissions from the API and assign them to the local component state. Then we pass them to the SubmissionsList component which is responsible for rendering the table. SubmissionsList:

 
import React from 'react';
import { Link } from 'react-router';

class SubmissionsList extends React.Component {
  render() {
    const headers = this.props.attributes.map(header => {
      return (
        <th key={`header-${header}`}>{header}</th>
      );
    });

    const body = this.props.submissions.map(submission => {
      const tr = this.props.attributes.map(attribute => {
        return (<td key={`value-${attribute}`}>{submission[attribute]}</td>);
      });

      return (
        <tr key={submission.id}>
          {tr}
          <td><Link to={`/submissions/${submission.id}`}>Show</Link></td>
        </tr>
      );
    });

    return (
      <table>
        <thead><tr>{headers}</tr></thead>
        <tbody>{body}</tbody>
      </table>
    )
  }
};

export default SubmissionsList;

 

Backend

To have some kind of backend, you can clone and setup this very simplified backend app. Just follow instructions in the README.

Starting the app!

Now we can finally test if everything works. Run npm start in the console, and go to http://localhost:3000 in your browser.

Rating

Now we can implement the rating feature itself.
Let’s add SubmissionPage:

 
import React from 'react';
import Connection from '../lib/Connection';
import Rate from './Rate';

class SubmissionPage extends React.Component {
  constructor(props) {
    super(props);
    this.state = { submission: {} };
  }

  componentDidMount() {
    const submission_id = this.props.params.id;
    Connection.get(`/submissions/${submission_id}`).then((response) => {
      this.setState({ submission: response.data });
    });
  }

  performRating(rate) {
    const submission = this.state.submission;
    Connection.post(`/submissions/${submission.id}/rate`, { rate: rate }).then(
      (response) => {
      this.setState({ submission: response.data });
    });
  }

  render() {
    const submission = this.state.submission;

    return (
      <div>
        <div className="submission">
          <h2>Submission</h2>
          <ul>
            <li>Id: {submission.id}</li>
            <li>First Name: {submission.first_name}</li>
            <li>Last Name: {submission.last_name}</li>
          </ul>
        </div>
        <Rate rate={submission.rate} performRating={this.performRating.bind(this)} />
      </div>
    )
  }
};

export default SubmissionPage;

 

Again, in componentDidMount we load particular submissions from the API and assign them to the local component state. But the most important part is this:

 
<Rate rate={submission.rate} performRating={this.performRating.bind(this)} />

 

We pass performRating handler as props to the Rate component:

 
import React from 'react';
import RateButton from './RateButton';

const AVAILABLE_RATES = [1, 2, 3, 4, 5];

class Rate extends React.Component {
  render() {
    const ratesInputs = AVAILABLE_RATES.map(value => {
      return <RateButton key={`rate-${value}`} value={value}
        performRating={this.props.performRating} />
    });
    return (
      <div>
        <div>{ratesInputs}</div>
        <div>{this.props.rate}</div>
      </div>
    )
  }
}

export default Rate;

 

And again pass performRating further, to the RateButton component, where we have actual rate value defined.

 
import React from 'react';

class RateButton extends React.Component {
  handleClick() {
    this.props.performRating(this.props.value);
  }

  render() {
    return (
      <button type='button' onClick={this.handleClick.bind(this)}>
        {this.props.value}
      </button>
    )
  }
}

export default RateButton;

 

Here, finally, we have it bound to the onClick event because only here do we know the particular value for a rating – this.props.value

Thanks to that, when a user clicks a rate button, the performRating method defined in SubmissionPage is called and a request to the API is made.

Let’s add a route to the src/routes.js to be able to access the view:

 
import React from 'react';
import { Route } from 'react-router';
import Main from './components/Main';
import PendingSubmissionsPage from './components/PendingSubmissionsPage';
import SubmissionFormPage from './components/SubmissionFormPage';
import SubmissionPage from './components/SubmissionPage';

export default (
  <Route path="/" component={Main}>
    <Route path="pending" component={PendingSubmissionsPage} />
    <Route path="submissions/new" component={SubmissionFormPage} />
    <Route path="submissions/:id" component={SubmissionPage} />
  </Route>
);

 

That’s all!

We just created a simple application using bare React.
The important thing to notice is that we hold the state of the app in many places. In a more complicated application, this can cause a lot of pain :)

In the next post, we’ll update our app to use a more structured pattern for managing the state – Flux.

For now, you can practise a bit by adding the missing EvaluatedSubmissionsPage and RejectedSubmissionsPage.

The full code is accessible here.

See you next week!

 

posted @ 2016-04-01 10:47  枪侠  阅读(197)  评论(0编辑  收藏  举报