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 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  # Verify virtual environment is activated
/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

Virtual environment explained

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. Your Python version might be different. Change the folder name (.../python3.7/... in this example) if needed.

$ pwd
/Users/awdeorio/src/eecs485/p3-insta485-clientside
$ python --version
Python 3.7.3
$ ls env/lib/python3.7/site-packages/
Flask-0.12.2.dist-info
Jinja2-2.9.6.dist-info
...

$NODE_PATH specifies the location of globally installed JavaScript packages. This environment variable was set when we activated our virtual environment. As a result, all globally installed JavaScript packages will 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 $NODE_PATH
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 based on changes to the project version.

$ 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.

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.

The previous command (npm install .) installed dependencies. Production dependencies are packages required by the application in production, (e.g., react). Development dependencies are packages only needed by the developer. 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

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 code below.

Add the following code to insta485/js/likes.jsx:

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

class Likes extends React.Component {
  /* Display number of likes and 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;

Add the following code to 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.

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

Build

The build process is described by webpack.config.js, which should live in the project root directory and is included with the starter files.

Run the front end build process. In this example, webpack is doing a similar job that make does for C++ programs.

$ 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. If you want to use it, run the command in a separate window and it will constantly run and 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. React is a declarative JavaScript library that mirrors object-oriented programming, allowing users to develop complex UIs using component classes. 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 used to contain the HTML for our server-side dynamic pages implementation. Eventually, we’re going to duplicate (almost) this entire page using React to implement a client-side dynamic page. Previously, you should have added a div with a unique ID of “reactEntry”. This will serve as the 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=""></script>
</body>
...

Check out the file insta485/js/main.jsx. First, it contains import statements for both third-party React libraries as well as our own custom component files. The import keyword is 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.

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. Additionally, props are passed down to child components through a parent component while state is maintained by the instance of this component, similar to inheritance in object-oriented programming: child classes inherit some variables and functions from parent classes, but can also manage child-class specific variables. 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 for the immutable props that are passed down by parent components. This is optional in React code, but it’s required by our style guide which is 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 information within the mutable state: the number of likes, and whether the logged in user likes this postid. Keep in mind that url is an item in the immutable this.props object that can be accessed with this.props.url. The constructor will initialize the mutable state object to default values and these values will be updated once a response from the REST API is received.

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 calls fetch(), which 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, the function in the second .then() clause following fetch() calls this.setState(), which will trigger a DOM update. Since this is an async function, the code inside the .then() clause that sets state will not run until the JSON data is received and processed from the .fetch() call. This concept of “I promise I will set state only after the JSON data is received and processed with no errors” is conveniently known as promises, which we promise to describe in more detail later.

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 other words, Promises allows us to write code that translates to “I promise I’ll do __ when the previous task of __ is done”. In this example, fetch helps us implement non blocking (asynchronous) 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 and asynchronous programming when you have potentially long-running, blocking operations that would cause latency spikes if done synchronously. 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.

Only after fetch() and .json() are finished running, the code inside the next .then() clause is run, which updates the numLikes state.

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

The render() method uses our component’s immutable properties (this.props.*, not in this example) and mutable state (this.state.*) and renders them into HTML, similar to how values were used to fill in the blanks for Jinja templates. The JSX extension to JavaScript allows us to use HTML syntax directly in JavaScript code.

JSX is a JavaScript language extension used by React. It is parsed by our front end compiler (webpack) and turned into widely supported ES5 code.

We’ll take a look at the Likes.render() method.

render() {
    // This line 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>
    );
  }

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.

Run eslint on one file, and on all files ending in .jsx in a directory.

$ 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 the HTML with your react component classes, 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 clear the cache and reload the JavaScript using the hard refresh. The commands for a hard refresh are different based on your OS and browser so take a look on how to hard refresh with your system. If you are using Chrome, you can also disable caching by going to the network tab of the web inspector developer tool and clicking on the checkbox that says “Disable cache”. This comic explains (credit: xkcd.com). refresh types