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:
- Download and install JavaScript libraries
- Package JavaScript libraries together with our own code for deployment
- Compile modern ES6 JavaScript syntax to portable ES5. If you’re interested, here’s a little JavaScript history.
- Compile JSX syntax to portable ES5. JSX is a language extension that lets you embed HTML in a JavaScript program. It’s like jinja2 templates, but in JavaScript
- Concatenate everything into one JS file for final client consumption
- Minify source code (optional)
- Obfuscate source code (optional)
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.
Python 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
Install Node
In order to use React, we must first install the Node JavaScript interpreter and npm package manager. At least the latest LTS version of Node is required for EECS 485. That’s the version on the Node home page that says “Recommended For Most Users.”
macOS
We will use Homebrew to install Node on macOS. Your versions may be different.
$ brew install node
$ node --version
v19.8.1
$ npm --version
9.5.1
Linux/WSL
If you have an old version of Node installed already, uninstall it first. Then we’ll install the latest version from a third-party package repository maintained by NodeSource using apt
.
$ sudo apt remove nodejs
$ sudo apt autoremove
$ sudo apt update
$ sudo apt install -y ca-certificates curl gnupg
$ sudo mkdir -p /etc/apt/keyrings
$ curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
$ NODE_MAJOR=23
$ echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list
$ sudo apt update
$ sudo apt install nodejs -y
$ node --version
v20.7.0
$ npm --version
10.1.0
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/post.jsx
Like Python’s pyproject.toml
, 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": {
"dayjs": "^1.11.9",
"prop-types": ">=15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-infinite-scroll-component": ">=6.1.0",
"ts-loader": ">=9.4.2",
"typescript": ">=5.0.2"
}
}
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 ci .
WSL Pitfall: npm
may be slow or produce errors on network file shares. WSL uses a network file share between the Linux and Windows file systems. Use a folder that’s not a network file share.
Bad Example | Good Example |
---|---|
/mnt/c/Users/awdeorio/ |
/home/awdeorio/ |
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 pyproject.toml
is to Python packages. The dependencies for our custom package were installed right next door to the dependencies in ./node_modules/
. Relevant modules in ./node_modules/
will later be bundled together for distribution by our web server.
The previous command (npm ci .
) 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 linters called eslint
and prettier
. 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 webpack --version
4.41.5
$ npx eslint --version
v6.8.0
$ npx prettier --version
2.7.1
JavaScript code
In this section, we will code a very small JavaScript module that fetches the image url and post owner for a post from our REST API and displays that information. 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.
Before continuing, read the React quick start.
Add the following code to insta485/js/post.jsx
:
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
// The parameter of this function is an object with a string called url inside it.
// url is a prop for the Post component.
export default function Post({ url }) {
/* Display image and post owner of a single post */
const [imgUrl, setImgUrl] = useState("");
const [owner, setOwner] = useState("");
useEffect(() => {
// Declare a boolean flag that we can use to cancel the API request.
let ignoreStaleRequest = false;
// Call REST API to get the post's information
fetch(url, { credentials: "same-origin" })
.then((response) => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then((data) => {
// If ignoreStaleRequest was set to true, we want to ignore the results of the
// the request. Otherwise, update the state to trigger a new render.
if (!ignoreStaleRequest) {
setImgUrl(data.imgUrl);
setOwner(data.owner);
}
})
.catch((error) => console.log(error));
return () => {
// This is a cleanup function that runs whenever the Post component
// unmounts or re-renders. If a Post is about to unmount or re-render, we
// should avoid updating state.
ignoreStaleRequest = true;
};
}, [url]);
// Render post image and post owner
return (
<div className="post">
<img src={imgUrl} alt="post_image" />
<p>{owner}</p>
</div>
);
}
Post.propTypes = {
url: PropTypes.string.isRequired,
};
Add the following code to insta485/js/main.jsx
:
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import Post from "./post";
// Create a root
const root = createRoot(document.getElementById("reactEntry"));
// This method is only called once
// Insert the post component into the DOM
root.render(
<StrictMode>
<Post url="/api/v1/posts/1/" />
</StrictMode>
);
Now we need to put reactEntry
somewhere in our HTML code and load the JavaScript. If you completed project 2, copy insta485/templates/
. If not, create an HTML file at insta485/templates/index.html
. Add the following to the page’s <body>
. If you copied it from project 2, go ahead and delete all the jinja template code that displays the feed, but keep the navigation bar.
<div id="reactEntry">
Loading ...
</div>
<!-- Load JavaScript -->
<script src="{{ url_for('static', filename='js/bundle.js') }}"></script>
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
Before you proceed, make sure you have a Flask endpoint set up for the index page. Copy insta485/views/
from project 2 if you have it. If you didn’t complete project 2, copy insta485/views/__init__.py
and insta485/views/index.py
from the Flask tutorial here. You should already have insta485/__init__.py
either from project 2 or from the REST API tutorial.
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
[webpack-cli] Compilation finished
asset bundle.js 1.86 MiB [compared for emit] (name: main)
runtime modules 1.03 KiB 5 modules
modules by path ./insta485/js/*.jsx 30.1 KiB 6 modules
...
4 modules
webpack 5.6.0 compiled successfully in 2290 ms
Pro-tip: In the future you may find this command useful. If you want to use it, run the command in a separate window in watch mode and 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 -m1 'function Post'
function Post(_ref) {
Finally, browse to http://localhost:8000/, where you’ll see the post widget.
React/JS code explained
In this section, we’ll explain how our React Post
component works. React is a declarative JavaScript library that allows users to develop complex UIs using components. 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 src="{{ url_for('static', filename='js/bundle.js') }}"></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 Post
Component to the reactEntry
div
in our HTML document.
In this example, we render one component, Post
. In your project you will render many components; however, you must still use a single createRoot()
and root.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.
Wrapping your React application in <StrictMode>
lets you find common bugs in your components early during development. <StrictMode>
causes your components to re-render and re-run Effects an additional time, helping uncover bugs related to impure rendering and missing Effect cleanup. See here for documentation.
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import Post from "./post";
// Create a root
const root = createRoot(document.getElementById("reactEntry"));
// This method is only called once
// Insert the post component into the DOM
root.render(
<StrictMode>
<Post url="/api/v1/posts/1/" />
</StrictMode>
);
Now, we’ll discuss the code in insta485/js/post.jsx
. It contains one component implemented as a function.
We’ll represent a single post as a Post
. This component is a pure function. It will always return the same output given the same input. Inputs are passed to a component via props. Props are passed to components as an argument. The Post component has a single prop, url
, so the argument Post
takes is in the form {url: string}
.
Since components are pure functions, props are read-only and immutable: components cannot change their props. We use state to create values that components can change. When mutable state changes, this causes the component to re-render, updating the UI.
It’s helpful to think of components as normal functions that just so happen to return HTML. Every time you include a <Post />
component, you’re calling the Post()
function.
After declaring the Post
component, we’re going to use the prop-types
library to add some 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.
Post.propTypes = {
url: PropTypes.string.isRequired,
};
Mutable state
This simplified Post
object will keep track of two pieces of information within the mutable state: the image URL, and the owner of the post. Keep in mind that url
is an item in the immutable props object that can be accessed with the variable url
. We’ll initialize two mutable state variables with default values and these values will be updated once a response from the REST API is received.
const [imgUrl, setImgUrl] = useState("");
const [owner, setOwner] = useState("");
We’re using the useState()
function to create our state. This function is called a “hook” because it hooks into React’s internal behavior. Its argument is the default value that’s given to the new state. It returns an array with two items, a state variable and a setter function. The syntax we use on the left-hand side of the equals sign is called array destructuring: it lets us read values from the array directly into independent variables. So imgUrl
holds the imgUrl
state at any given time, and setImgUrl
is a function that lets us change the value of imgUrl
.
State is unique to every instance of a component. In other words, every Post
will call useState()
independently and have unique copies of each state. React keeps track of which state is associated with which component. Whenever you call setOwner("NEW_OWNER_NAME")
inside a Post
, React will re-render that specific Post
and update owner
with "NEW_OWNER_NAME"
.
Please read the Adding Interactivity chapter of the React Docs to learn more about state.
A note on class-based components
If you look at old EECS 485 exams or legacy React code, you might see examples of React components that are written as classes instead of functions. In earlier versions of React, this was required in order for components to be stateful. Thanks to hooks, now almost every component can be written as a function. Class-based components are still supported in React, but nearly all new React components are functional. We recommend that you avoid class-based components. You’ll only ever need to learn about them if you need to read and understand an old codebase. You can read about them in the React Docs’ Legacy APIs section.
Here’s a simple example of a class-based component:
import React from "react";
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}!</h1>;
}
}
Adding AJAX calls to the REST API
We want to make sure that when the user sees the Post
component, it has data taken from our REST API. We don’t know the imgUrl
or owner
of this post at render-time: we only know what we pass in as props, which in this case is url
, the API endpoint for the post. Since we don’t have the data we need at render time, we create an effect using the useEffect()
hook. useEffect()
takes a callback function as an argument. The function that you pass into useEffect()
is called by React after the component renders. It’s completely outside of the rendering process, so we can do anything in it, like making asynchronous requests and changing state.
As a reminder, the results of a call to the API look like this:
{
"comments": [
{
"commentid": 5,
"lognameOwnsThis": false,
"owner": "jflinn",
"ownerShowUrl": "/users/jflinn/",
"text": "Walking the plank #chickensofinstagram",
"url": "/api/v1/comments/5/"
},
{
"commentid": 6,
"lognameOwnsThis": true,
"owner": "awdeorio",
"ownerShowUrl": "/users/awdeorio/",
"text": "This was after trying to teach them to do a #crossword",
"url": "/api/v1/comments/6/"
}
],
"created": "2021-05-06 19:52:44",
"imgUrl": "/uploads/122a7d27ca1d7420a1072f695d9290fad4501a41.jpg",
"likes": {
"lognameLikesThis": true,
"numLikes": 3,
"url": "/api/v1/likes/1/"
},
"owner": "awdeorio",
"ownerImgUrl": "/uploads/e1a7c5c32973862ee15173b0259e3efdb6a391af.jpg",
"ownerShowUrl": "/users/awdeorio/",
"postShowUrl": "/posts/1/",
"postid": 1,
"url": "/api/v1/posts/1/"
}
And here is the code we use for creating the effect:
useEffect(() => {
// Declare a boolean flag that we can use to cancel the API request.
let ignoreStaleRequest = false;
// Call REST API to get the post's information
fetch(url, { credentials: "same-origin" })
.then((response) => {
if (!response.ok) throw Error(response.statusText);
return response.json();
})
.then((data) => {
// If ignoreStaleRequest was set to true, we want to ignore the results of the
// the request. Otherwise, update the state to trigger a new render.
if (!ignoreStaleRequest) {
setImgUrl(data.imgUrl);
setOwner(data.owner);
}
})
.catch((error) => console.log(error));
return () = > {
// This is a cleanup function that runs whenever the Post component
// unmounts or re-renders. If a Post is about to unmount or re-render,
// should avoid updating state.
ignoreStaleRequest = true;
};
}, [url]);
Pay close attention to the last line in the code snippet above. The second argument to useEffect()
is a dependency array. The variables in the dependency array are what cause the effect to run. If you don’t include a dependency array, the effect will run every time the component is rendered no matter what. If you include an empty array, the effect will only run once, after the component’s first render. If you include an array with variables, like props or state, the effect will run after the first render and again every time one of the variables in the array changes. Any variables declared outside of the effect that are used within the effect are dependencies, so they should be included in the array. You’ll get a linting error if you ignore this.
Also note our use of the ignoreStaleRequest
variable. A function passed into useEffect()
can return a cleanup function that runs whenever the component either unmounts or re-renders. Whenever that happens, we set ignoreStaleRequest
to true
, because we don’t want to set state based on stale data. Any time the component renders, it should trigger a brand new request to the REST API and not have to worry about any previous requests.
After receiving JSON data, the function in the second .then()
clause following fetch()
calls setImgUrl()
and setOwner()
, which will trigger the entire component to re-render. That means the Post()
function will run again and return new JSX based on the updated values in the state variables. 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.
The React docs Synchronizing With Effects section is helpful for learning more about effects.
For a detailed explanation of the nuances of fetch
and React, see our tutorial on data fetching with useEffect.
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
.
Promise
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, Promise
s 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:
fetch()
, which requests a network resource that might take long time to respond. Resolves network response toResponse
object after server responds..json()
which takesResponse
stream and resolves it to a JSON object, which might also be long running.
Only after fetch()
is finished running, the code inside the .then()
clause is run, which calls .json()
. Since .json()
is also asynchronous, the next .then()
, which updates the imgUrl
and owner
, does not run until .json()
is finished 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
A component should return JSX. The object returned by our component uses immutable props (url
, not in this example) and mutable state (imgUrl
and owner
) 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 return value of the component.
// Render single post
return (
<div className="post">
<img src={imgUrl} alt="post_image" />
<p>{owner}</p>
</div>
);
Please read the Describing the UI chapter of the React Docs to learn more about using JSX to render React components.
Developer Tools
In this section, we’ll discuss using eslint
and prettier
to enforce coding style and a web browser extension that helps debug React applications.
eslint
We use eslint
to enforce the AirBnB JavaScript coding standard. The configuration is in the .eslintrc.js
provided with the starter files.
$ npx eslint insta485/js/post.jsx # Check one file
$ npx eslint --ext jsx insta485/js/ # Check all files
prettier
We use prettier
to enforce default formatting rules. You can also have prettier
fix formatting automatically.
$ npx prettier --check insta485/js # Check
$ npx prettier --write insta485/js # Fix
React Developer Tools
React Developer Tools is a web browser extension that adds a “Components” tab to the developer tools. It lets you browse your React components organized in a way that looks a lot like your JSX code, rather than the complex DOM that results from your source code. See the React/JS Debugging Tutorial for an example of how to use this tool.
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).
Acknowledgments
Original document written by Andrew DeOrio awdeorio@umich.edu.
This document is licensed under a Creative Commons Attribution-NonCommercial 4.0 License. You’re free to copy and share this document, but not to sell it. You may not share source code provided with this document.