p3-insta485-clientside

React/JS Tutorial

This tutorial will walk you through a simple React/JS application, including JavaScript development tools.

Install tool chain

We need to install a tool chain for our front end. The tool chain provides a few benefits:

Our tool chain will consist of a JavaScript interpreter (node) and a package manager (npm). Keep in mind that we aren’t running any servers using JavaScript. The tool chain used by JavaScript programmers happens to be written in JavaScript so we’ll need a JavaScript interpreter to run it.

Install JavaScript virtual environment

This tutorial assumes you’ve already created a Python virtual environment. If you need to create one, use the Python Virtual Environment Tutorial. Don’t forget to activate your virtual environment every time you open your terminal.

$ pwd
/Users/awdeorio/src/eecs485/p3-insta485-clientside
$ source env/bin/activate
$ echo $VIRTUAL_ENV
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env

We want to install our front end tool chain into our existing virtual environment. nodeenv is a Python tool for merging a Python virtual environment with a JavaScript virtual environment.

$ pip install nodeenv
$ nodeenv --python-virtualenv
$ deactivate
$ source env/bin/activate  # Reactivate

Understanding the virtual environment

We now have a complete local environment for both our back end tool chain and our front end tool chain. Environment variables point to these virtual environments.

$ echo $VIRTUAL_ENV
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env
$ echo $NODE_VIRTUAL_ENV
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env

We have two interpreters, one for Python and one for JavaScript. Both are installed in the virtual environment.

$ which python
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/python
$ which node
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/node

There are two package managers, one for Python and one for JavaScript. Both are installed in the virtual environment.

$ which pip
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/pip
$ which npm
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/bin/npm

Python packages live in the virtual environment.

$ ls env/lib/python3.6/site-packages/
Flask-0.12.2.dist-info
Jinja2-2.9.6.dist-info
...

Globally installed JavaScript packages live in the virtual environment. So far, the npm package manager is the only globally installed JavaScript package.

$ echo $NODE_PATH
/Users/awdeorio/src/eecs485/p3-insta485-clientside/env/lib/node_modules
$ ls env/lib/node_modules/
npm

JavaScript packages

Next, we’ll create a JavaScript package for our front end source code.

Source code will live in insta485/js/. We’re going to be using the JSX extension to JavaScript to embed HTML inside JavaScript code, so name the files with .jsx extensions.

$ mkdir insta485/js/
$ touch insta485/js/main.jsx
$ touch insta485/js/likes.jsx

Like Python’s setup.py, JavaScript packages have a configuration file. This file specifies what version of each package will be downloaded when you run the install command later.

To ensure you have the exact same versions of JavaScript libraries as we do, use package-lock.json and package.json from the starter files. Your versions may be different.

$ pwd
/Users/awdeorio/src/eecs485/p3-insta485-clientside
$ cat package.json
{
  "name": "insta485",
  ...
  "dependencies": {
    "jsonlint": "1.6.3",
    "moment": "2.24.0",
    "prop-types": "15.7.2",
    "react": "16.12.0",
    "react-dom": "16.12.0",
    "react-infinite-scroll-component": "5.0.4"
  }
}

Our front end JavaScript package will use the react library, which in turn relies on several dependencies of its own. We’ll use npm to install these dependencies.

$ pwd
/Users/awdeorio/src/eecs485/p3-insta485-clientside
$ npm install .

Pitfall: npm doesn’t play nice with network file shares, for example, a shared folder between a guest virtual machine and its host machine. If you’re working inside a virtual machine, be sure that you’re not in a shared folder.

The previous command installed dependencies and development dependencies. There are two command line utilities that we’ll use in development. A JavaScript compiler (or “transpiler”) called webpack and a linter called eslint. These tools are installed in ./node_modules. npm provides the useful command npx to designate that we should include this folder in our PATH. For example npx webpack is equivalent to ./node_modules/.bin/webpack.

$ npx eslint --version
v6.8.0
$ npx webpack --version
4.41.5

What just happened? Our module’s dependencies were described by package.json, which is an input to npm. package.json is to JavaScript packages as setup.py is to Python packages. The dependencies for our custom package were installed right next door to ./package.json in ./node_modules/. Note that this is distinct from env/lib/node_modules/. The files in ./node_modules/ will later be bundled together for distribution by our web server, while env/lib/node_modules/ will not be distributed.

JavaScript code

In this section, we will code a very small JavaScript module that fetches the number of likes for a post from our REST API and displays it. We’ll write modern ES6 source code and transpile it to older ES5. (A “transpiler” is a compiler that takes source code and generates equivalent source code in a different language (rather than the executable binary that a traditional compiler generates.)) In a later section, we’ll go back over this source code and explain in more detail about how it works.

(Also: why bother doing this? Well, ES6 has some nice features but isn’t yet supported by all browsers. By transpiling ES6 to ES5, we get the nice advantages of ES6 language features, but the broad support of ES5.)

Note that you might find it useful to go through the React tutorial before reading the below code.

Write insta485/js/likes.jsx:

import React from 'react';
import PropTypes from 'prop-types';

class Likes extends React.Component {
  /* Display number of likes a like/unlike button for one post
   * Reference on forms https://facebook.github.io/react/docs/forms.html
   */

  constructor(props) {
    // Initialize mutable state
    super(props);
    this.state = { numLikes: 0 };
  }

  componentDidMount() {
    // This line automatically assigns this.props.url to the const variable url
    const { url } = this.props;

    // Call REST API to get number of likes
    fetch(url, { credentials: 'same-origin' })
      .then((response) => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
      .then((data) => {
        this.setState({
          numLikes: data.likes_count,
        });
      })
      .catch((error) => console.log(error));
  }

  render() {
    // This line automatically assigns this.state.numLikes to the const variable numLikes
    const { numLikes } = this.state;

    // Render number of likes
    return (
      <div className="likes">
        <p>
          {numLikes}
          {' '}
          like
          {numLikes !== 1 ? 's' : ''}
        </p>
      </div>
    );
  }
}

Likes.propTypes = {
  url: PropTypes.string.isRequired,
};

export default Likes;

Write insta485/js/main.jsx:

import React from 'react';
import ReactDOM from 'react-dom';
import Likes from './likes';

// This method is only called once
ReactDOM.render(
  // Insert the likes component into the DOM
  <Likes url="/api/v1/p/1/likes/" />,
  document.getElementById('reactEntry'),
);

Now we need to put reactEntry somewhere in our HTML code and load the JavaScript. Add this to the <body> of your template for the main page insta485/templates/index.html. Also, go ahead and delete all the jinja template code that displays the feed.


...
<body>
  <!-- Plain old HTML and jinja2 nav bar goes here -->


  <div id="reactEntry">
    Loading ...
  </div>
  <!-- Load JavaScript -->
  <script type="text/javascript" src="{{ url_for('static', filename='js/bundle.js') }}"></script>
</body>
...

Notice that the HTML code asks for bundle.js, which is the output of our front end build process. The inputs to the front end build process are the JavaScript files in insta485/js/. The output is a single JavaScript file that is completely self-contained with no dependencies, insta485/static/js/bundle.js.

Build

The build process is described by webpack.config.js, which should live in the project root directory. Use the version provided in the starter files, which looks something like this:

const path = require('path');

module.exports = {
  entry: './insta485/js/main.jsx',
  output: {
    path: path.join(__dirname, '/insta485/static/js/'),
    filename: 'bundle.js',
  },
  module: {
    loaders: [
      {
        // Test for js or jsx files
        test: /\.jsx?$/,
        loader: 'babel-loader',
        query: {
          // Convert ES6 syntax to ES5 for browser compatibility
          presets: ['env', 'react'],
        },
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
};

Run the front end build process.

$ npx webpack
Hash: 94d02a55e475959ef08d
Version: webpack 2.6.1
Time: 4910ms
    Asset    Size  Chunks                    Chunk Names
bundle.js  769 kB       0  [emitted]  [big]  main
   [0] ./~/process/browser.js 5.45 kB {0} [built]
  [15] ./~/react/lib/ReactElement.js 11.6 kB {0} [built]
  [17] ./~/react-dom/lib/ReactReconciler.js 6.29 kB {0} [built]
  [18] ./~/react/lib/React.js 5.11 kB {0} [built]
  [50] ./~/react/react.js 55 bytes {0} [built]
  [83] ./insta485/js/likes.jsx 3.52 kB {0} [built]
  [84] ./~/react-dom/index.js 58 bytes {0} [built]
  [85] ./insta485/js/main.jsx 492 bytes {0} [built]
 [103] ./~/prop-types/index.js 1.4 kB {0} [built]
 [117] ./~/react-dom/lib/ReactDOM.js 5.19 kB {0} [built]
 [166] ./~/react-dom/lib/findDOMNode.js 2.46 kB {0} [built]
 [177] ./~/react/lib/ReactDOMFactories.js 5.48 kB {0} [built]
 [179] ./~/react/lib/ReactPropTypes.js 500 bytes {0} [built]
 [181] ./~/react/lib/ReactVersion.js 350 bytes {0} [built]
 [183] ./~/react/lib/createClass.js 688 bytes {0} [built]
    + 172 hidden modules

In the future you may find this command useful. It will automatically rebuild whenever changes are detected.

$ npx webpack --watch 

What just happened? All the libraries needed by our JavaScript app were concatenated into bundle.js. Then, it was transpiled to ES5, which is supported by all modern web browsers.

Fire up your web server again. You can see that our bundle.js is available.

$ ./bin/insta485run
$ curl -s "http://localhost:8000/static/js/bundle.js" | grep Likes -m1
var Likes = function (_React$Component) {

Finally, browse to http://localhost:8000/, where you’ll see the likes widget.

React/JS code explained

In this section, we’ll explain how our React Likes component works. A component lets you split the UI into independent, reusable pieces, and think about each piece in isolation. The code we’re describing here is a straight-forward adaptation of the examples in the React Docs.

Let’s start with code we’re already familiar with. Our template for the root URL / lives in insta485/templates/index.html. This file already contains the server-side dynamic pages implementation that uses jinja2 templates. Eventually, we’re going to duplicate (almost) this entire page using client-side dynamic pages. You’ll see that there is a React entry point. Basically, that’s a div with a unique name that the browser’s JavaScript interpreter will later fill in with content. You’ll also see that we loaded our bundle.js JavaScript.


...
<body>
  <!-- Plain old HTML and jinja2 nav bar goes here -->


  <div id="reactEntry">
    Loading ...
  </div>
  <!-- Load JavaScript -->
  <script type="text/javascript" src="{{ url_for('static', filename='js/bundle.js') }}"></script>
</body>
...

Check out the top level JavaScript file insta485/js/main.jsx. First, it contains import statements, which are an ES6 language feature. Keep in mind that these are transpiled to interoperable ES5 by our front end build tool chain. Then, it connects a custom Likes Component to the reactEntry div in our HTML document. Recall that Component is a React technical term.

In this example, we render one component, Likes. In your project you will render many components, however, you must still use a single ReactDOM.render() call. You should use one parent component that calls other child components. See this section of the React Docs. You may also take this opportunity to carefully read through the Thinking in React docs.

import React from 'react';
import ReactDOM from 'react-dom';
import Likes from './likes';

// This method is only called once
ReactDOM.render(
  // Insert the likes component into the DOM
  <Likes url="/api/v1/p/1/likes/" />,
  document.getElementById('reactEntry'),
);

Now, we’ll discuss the code in insta485/js/likes.jsx. It contains one component implemented with a class. Classes are an ES6 JavaScript language feature.

We’ll represent the number of likes as a Likes object. This Component is class-type and contains mutable state. Inside the class’s methods, immutable properties appears as this.props.MY_INPUT_NAME. Mutable state appears as this.state.MY_STATE_VARIABLE. Why is it important to separate mutable state from immutable properties? Because when mutable state changes, this triggers a chain reaction to update the UI. More in the docs.

class Likes extends React.Component {
  constructor(props) {
    // Runs when an instance is created.
    // Initialize mutable state here
 }

  componentDidMount() {
    // Runs when an instance is added to the DOM
    // Make your first AJAX call here
  }

  render() {
    // Returns HTML representing this component
    // Use JSX template-like syntax here
  }
}

After declaring the Likes object, we’re going to use the prop-types library to add some compile-time static type checking. This is optional in React code, but it’s required by our style guide (Described later, in the eslint section).

Likes.propTypes = {
  url: PropTypes.string.isRequired,
};

Constructors and mutable state

The Likes object will keep track of two pieces of mutable state: the number of likes, and whether the logged in user likes this postid. Keep in mind that url is an immutable input that shows up as this.props.url. Our mutable state is obtained from the REST API. The constructor will initialize the mutable state to default values.

constructor(props) {
  // Initialize mutable state
  super(props);
  this.state = { num_likes: 0, logname_likes_this: false };
}

Adding AJAX calls to the REST API

When an instance of our Likes component is added to the DOM, React calls our componentDidMount() method. This method makes an AJAX call to get the number of likes from the REST API at /api/v1/p/<postid_url_slug>/likes/, which was passed as an input via this.props.url. As a reminder, the results of a call to the API look like this:

{
  "logname_likes_this": 1,
  "likes_count": 3,
  "postid": 1,
  "url": "/api/v1/p/1/likes/"
}

After receiving JSON data, componentDidMount() calls this.setState(), which will trigger a DOM update.

componentDidMount() {
    // This line automatically assigns this.props.url to the const variable url
    const { url } = this.props;

    // Call REST API to get number of likes
    fetch(url, { credentials: 'same-origin' })
      .then((response) => {
        if (!response.ok) throw Error(response.statusText);
        return response.json();
      })
      .then((data) => {
        this.setState({
          numLikes: data.likes_count,
        });
      })
      .catch((error) => console.log(error));
  }

fetch

Now take a look at the call to fetch. The fetch API is a nice way to use native JavaScript (without jQuery) to make requests to a REST API. This blog post does a nice job of explaining fetch.

Promises

The fetch method returns a Promise object. A Promise is an abstraction for an asynchronous task. For example, it can express the idea of “please do this after my data returns” in a succinct manner. Here is Google’s explanation of Promises. In this example, fetch helps us implement non blocking code, while retaining the “make the request first, then process the response second” ordering.

Why are there two then clauses? Because there are two promises! You use promises when you have asynchronous, potentially long running operations. In this case there are 2 such operations:

  1. fetch(), which requests a network resource that might take long time to respond. Resolves network response to Response object after server responds.
  2. .json() which takes Response stream and resolves it to a JSON object, which might also be long running.

Arrow functions =>

What are those arrows =>? Arrow functions implement lambda expressions in JavaScript. A lambda expression gives us an anonymous function. Here’s an example, first using an ES5 style lambda, and second using an ES6 style lambda (arrow function):

$ node
> [1,2,3,4].filter(function(x) {return x % 2 === 0});
[ 2, 4 ]
> [1,2,3,4].filter(x => x % 2 === 0);
[ 2, 4 ]

Turning state into HTML elements

Finally, we’ll fill in the Likes.render() method.

render() {
    // This line automatically assigns this.state.numLikes to the const variable numLikes
    const { numLikes } = this.state;

    // Render number of likes
    return (
      <div className="likes">
        <p>
          {numLikes}
          {' '}
          like
          {numLikes !== 1 ? 's' : ''}
        </p>
      </div>
    );
  }

The first thing to notice is that the render() method converts our component’s immutable properties (this.props.*, not in this example) and mutable state (this.state.*) into HTML. We’re using HTML syntax directly in JavaScript code, which is the JSX extension to JavaScript.

JSX is a JavaScript language extension used by React. It is possible to use React without JSX. An alternative is nested React.createElement() calls. JSX is a syntactic convenience. It is parsed by our front end compiler (webpack) and turned into widely supported ES5 code.

If you want to play with JavaScript transpiling, you might enjoy the interactive online version of the Babel compiler. Here it is pre-loaded with a Hello World example

Developer Tools

In this section, we’ll discuss using eslint to enforce coding style and a Google Chrome web browser extension that helps debug React applications.

eslint

We can enforce coding style using eslint. eslint’s configuration lives in .eslintrc.js and is provided with the starter files. We’ll configure eslint to enforce the AirBnB JavaScript coding standard. An example of .eslintrc.js using the AirBnB JS coding standard can be seen here:

module.exports = {
  "extends": "airbnb",
  "plugins": [
    "react",
    "jsx-a11y",
    "import"
  ],
  "env": {
      "browser": true,
  },
  "rules": {
    "no-console": "off",
  }
};

Run eslint.

$ npx eslint insta485/js/likes.jsx
$ npx eslint --ext jsx insta485/js/

React Developer Tools for Chrome

The React Developer Tools for Chrome is a web browser extension that adds a “React” tab to the Chrome developer tools. It lets your browse your React components organized in a way that looks a lot like your source code, rather than the complex DOM that results from your source code.

Browser refresh and JavaScript

JavaScript source code is sometimes cached by the web browser. If you change the source code, you need to tell your browser to reload the JavaScript. This comic explains (credit: xkcd.com). refresh types