Interactive Sites

November 2018: This material is under very active development, and we would appreciate your help: please email us, file an issue in our GitHub repository, or submit a pull request. (We would particularly appreciate descriptions of common errors and how to fix them.) Everyone whose work is incorporated will be acknowledged; please note that all contributors are required to abide by our Code of Conduct.

Questions

  • How do I tell the browser what to do when someone clicks a button?
  • How should I structure my code to make interactions manageable?
  • How can a web application get data from a server?
  • How does modern JavaScript handle asynchronous operations?

Browsers allow us to define event handlers to specify what to do in response to an externally-triggered action, such as a page loading or a user pressing a button. These event handlers are just callback functions that are (usually) given an event object containing information about what happened, and while we can write them in pure JavaScript, they’re even easier to build in React.

Let’s switch back to single-page examples for a moment to show how we pass a callback function as a specifically-named property of the thing whose behavior we are specifying:

  <body>
    <div id="app"></div>
    <script type="text/babel">
      let counter = 0;
      const sayHello = (event) => {
        counter += 1
        console.log(`Hello, button: ${counter}`)
      }

      ReactDOM.render(
        <button onClick={sayHello}>click this</button>,
        document.getElementById("app")
      )
    </script>
  </body>

As its name suggests, a button’s onClick handler is called whenever the button is clicked. Here, we are telling React to call sayHello, which adds one to the variable counter and then prints its value.

Global variables and functions are a poor way to structure code. It’s far better to define the component as a class and then use a method as the event handler:

  <body>
    <div id="app"></div>
    <script type="text/babel">
      class Counter extends React.Component {

        constructor (props) {
          super(props)
          this.state = {counter: 0}
        }

        increment = (event) => {
          this.setState({counter: this.state.counter+1})
        }

        render = () => {
          return (
            <p>
              <button onClick={this.increment}>increment</button>
              <br/>
              current: {this.state.counter}
            </p>
          )
        }
      }

      ReactDOM.render(
        <Counter />,
        document.getElementById("app")
      )
    </script>
  </body>
</html>

Working from bottom to top, the ReactDOM.render call inserts whatever HTML is produced by <Counter/> into the element whose ID is "app". In this case, though, the counter is not a function, but a class with three parts:

  1. Its constructor passes the properties provided by the user up to React.Component’s constructor. (There aren’t any properties in this case, but there will be in future examples, so it’s a good habit to get into.) The constructor then creates a property called state that holds this component’s state. This property must have this name so that React knows to watch it for changes.

  2. The increment method uses setState (inherited from React.Component) to change the value of the counter. We must do this rather than creating and modifying this.counter so that React will notice the change in state and re-draw what it needs to.

  3. The render method takes the place of the functions we have been using so far. It can do anything it wants, but must return some HTML (using JSX). Here, it creates a button with an event handler and displays the current value of the counter.

React calls each component’s render method each time setState is used to update the component’s state; this is an example of a protocol, which was described earlier. Behind the scenes, React does some thinking to minimize how much redrawing takes place: while it may look as though the paragraph, button, and current count are all being redrawn each time, React will only actually redraw as little as it can.

But It Doesn’t Work

If we try running this little application from the command line with Parcel:

npm run dev -- src/interactive/display-counter.html

everything works as planned. But now try taking the code out of the web page and putting it in its own file:

<html>
  <head>
    <meta charset="utf-8">
    <title>Counter</title>
    <script src="app.js" async></script>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
import React from 'react'
import ReactDOM from 'react-dom'

class Counter extends React.Component {

  constructor (props) {
    ...as before...
  }

  increment = (event) => {
    this.setState({counter: this.state.counter+1})
  }

  render = () => {
    ...as before...
  }
}

ReactDOM.render(
  <Counter />,
  document.getElementById('app')
)

Let’s try running this:

`npm run dev -- src/interactive/counter/index.html
```text
> js-vs-ds@0.1.0 dev /Users/stj/js-vs-ds
> parcel serve -p 4000 "src/interactive/counter/index.html"

Server running at http://localhost:4000 
!!  /Users/stj/js-vs-ds/src/interactive/counter/app.js:11:12: Unexpected token (11:12)
   9 |   }
  10 | 
> 11 |   increment = (event) => {
     |             ^
  12 |     this.setState({counter: this.state.counter+1})
  13 |   }
  14 |

It seems that Parcel doesn’t like fat arrow methods. This happens because React is still using ES6 JavaScript by default, and fat arrow methods weren’t included in JavaScript at that point. All right, let’s try using “normal” function-style method definitions instead:

...imports as before...

class Counter extends React.Component {

  constructor (props) {
    super(props)
    this.state = {counter: 0}
  }

  increment (event) {
    this.setState({counter: this.state.counter+1})
  }

  render () {
    return (
      <p>
        <button onClick={this.increment}>increment</button>
        <br/>
        current: {this.state.counter}
      </p>
    )
  }
}

...render as before...

Parcel runs this without complaint, but clicking on the button doesn’t change the display. Despair is once again our friend—our only friend—but we persevere. When we open the debugging console in the browser, we see the message TypeError: this is undefined. The appendix explains in detail why this happens; for now, suffice to say that some poor choices were made early in JavaScript’s development about variable scoping.

At this point it appears that we can compile but not run, or not bundle files together. But wait: when we used an in-page script, we specified the type as text/babel and loaded https://unpkg.com/babel-standalone@6/babel.js in the page header along with React. Can Babel save us?

The answer is “yes”, though it takes a fair bit of searching on the web to find this out (particularly if you don’t know what you’re looking for). The magic is to create a file in the project’s root directory called .babelrc and add the following lines:

{
  "presets": [
    "react"
  ],
  "plugins": [
    "transform-class-properties"
  ]
}

Once we’ve done this, we can use NPM to install babel-preset-react and babel-plugin-transform-class-properties and then switch back to fat arrow methods. Voila: everything works.

What’s happening here is that when Babel translates our sparkly modern JavaScript into old-fashioned JavaScript compatible with all browsers, it reads .babelrc and obeys that configuration. The settings above tell it to do everything React needs using the transform-class-properties plugin; in particular, to accept fat arrow method definitions and bind this correctly. This works, but is a form of madness: something outside our program determines how that program is interpreted, and the commands controlling it go in yet another configuration file. Still, it is a useful form of madness, so we will press on.

Models and Views

Well-designed applications separate models (which store data) from views (which display it) so that each can be tested and modified independently. When we use React, the models are typically classes, and the views are typically pure functions.

To introduce this architecture, let’s re-implement the counter using:

  • App to store the state and provides methods for altering it,
  • NumberDisplay to display a number, and
  • UpAndDown to provide buttons that increment and decrement that number.

The crucial design feature is that NumberDisplay and UpAndDown don’t know what they’re displaying or what actions are being taken on their behalf, which makes them easier to re-use. Again, we will load everything manually instead of bundling to make dependencies clearer. The whole page is:

<html>
  <head>
    <meta charset="utf-8">
    <title>Hello World</title>
    <script src="https://fb.me/react-15.0.1.js"></script>
    <script src="https://fb.me/react-dom-15.0.1.js"></script>
    <script src="https://unpkg.com/babel-standalone@6/babel.js"></script>
    <script src="NumberDisplay.js" type="text/babel"></script>
    <script src="UpAndDown.js" type="text/babel"></script>
    <script src="app.js" type="text/babel"></script>
  </head>
  <body>
    <div id="app"></div>
    <script type="text/babel">
      ReactDOM.render(
        <App />,
        document.getElementById("app")
      )
    </script>
  </body>
</html>

The NumberDisplay class takes a label and a value and puts them in a paragraph (remember, the label and value will appear in our function as properties of the props parameter):

const NumberDisplay = (props) => {
  return (<p>{props.label}: {props.value}</p>)
}

Similarly, UpAndDown expects two functions as its up and down properties, and makes each the event handler for an appropriately-labelled button:

const UpAndDown = (props) => {
  return (
    <p>
      <button onClick={props.up}> [+] </button>
      <button onClick={props.down}> [-] </button>
    </p>
  )
}

We are now ready to build the overall application. It create a state containing a counter and defines methods to increment or decrement the counter’s value. Its render method then lays out the buttons and the current state using those elements:

class App extends React.Component {

  constructor (props) {
    super(props)
    this.state = {counter: 0}
  }

  increment = (event) => {
    this.setState({counter: this.state.counter + 1})
  }

  decrement = (event) => {
    this.setState({counter: this.state.counter - 1})
  }

  render = () => {
    return (
      <div>
        <UpAndDown up={this.increment} down={this.decrement} />
        <NumberDisplay label='counter' value={this.state.counter} />
      </div>
    )
  }
}

FIXME-22: diagram

This may seem pretty complicated, and it is, because this example would be much simpler to write without all this indirection. However, this strategy is widely used to manage large applications: data and event handlers are defined in one class, then passed into display components to be displayed and interacted with.

Fetching Data

Let’s use what we’ve learned to look at how the world might end. NASA provides a web API to get information about near-approach asteroids. We will use it to build a small display with:

  • a text box for submitting a starting date (get one week by default), and
  • a list of asteroids in that time period.

Here’s the first version of our App class:

class App extends React.Component {

  constructor (props) {
    super(props)
    this.state = {
      // ...fill in...
    }
  }

  onNewDate = (text) => {
    // ...fill in...
  }

  render = () => {
    return (
      <div>
        <DateSubmit newValue={this.onNewDate} />
        <AsteroidList asteroids={this.state.asteroids} />
      </div>
    )
  }
}

We’ll test it by displaying asteroids using fake data; as in our first example, the display component AsteroidList doesn’t modify data, but just displays it:

const AsteroidList = (props) => {
  return (
    <table>
      <tr><th>Name</th><th>Date</th><th>Diameter (m)</th><th>Approach (km)</th></tr>
      {props.asteroids.map((a) => {
        return (
          <tr>
            <td>{a.name}</td>
            <td>{a.date}</td>
            <td>{a.diameter}</td>
            <td>{a.distance}</td>
          </tr>
        )
      })}
    </table>
  )
}

AsteroidList expects data to arrive in props.asteroids, so let’s put some made-up values in App for now that we can then pass in:

class App extends React.Component {

  constructor (props) {
    super(props)
    this.state = {
      asteroids: [
        {name: 'a30x1000', date: '2017-03-03', diameter: 30, distance: 1000},
        {name: 'a5x500', date: '2017-05-05', diameter: 5, distance: 500},
        {name: 'a2000x200', date: '2017-02-02', diameter: 2000, distance: 200}
      ]
    }
  }

  ...
}

Let’s also create placeholder for DateSubmit:

const DateSubmit = (props) => {
  return (<p>DateSubmit</p>)
}

and run it:

FIXME-23: screenshot

The next step is to handle date submission. Since we’re trying to instill good practices, we will make a reusable component whose caller will pass in:

  • a text label;
  • a variable to update with the current value of a text box;
  • a function to call when text box’s value changes, and
  • another function to call when button clicked to submit.
const DateSubmit = ({label, value, onChange, onCommit}) => {
  return (
    <p>
      {label}:
      <input type="text" value={value} onChange={(event) => onChange(event.target.value)} />
      <button onClick={(event) => onCommit(value)}>new</button>
    </p>
  )
}

Note the use of destructuring in DateSubmit’s parameter list; this was introduced earlier and is an easy way to pull values out of the props parameter.

It’s important to understand order of operations in the example above. value={value} puts a value in the input box to display each time DateSubmit is called. We re-bind onChange and onClick` to functions on each call as well (remember, JSX gets translated into function calls). So yes, this whole paragraph is being re-created every time someone types, but React and the browser work together to minimize recalculation.

Now let’s go back and re-work our application:

class App extends React.Component {

  constructor (props) {
    super(props)
    this.state = {
      newDate: '',
      asteroids: [...]
    }
  }

  onEditNewDate = (text) => {
    this.setState({newDate: text})
  }

  onSubmitNewDate = (text) => {
    console.log(`new date ${text}`)
    this.setState({newDate: ''})
  }

  render = () => {
    return (
      <div>
        <h1>Asteroids</h1>
        <DateSubmit
          label='Date'
          value={this.state.newDate}
          onChange={this.onEditNewDate}
          onCommit={this.onSubmitNewDate} />
        <AsteroidList asteroids={this.state.asteroids} />
      </div>
    )
  }
}

It’s safe to pass this.state.newDate to value because we’re re-drawing each time there’s a change; remember, we’re passing a value for display, not a reference to be modified. And note that we are not doing any kind of validation: the user could type abc123 as a date and we would blithely try to process it.

It’s now time to get real data, which we will do using fetch with a URL. This returns a promise, so we’ll handle the result of the fetch in the promise’s then method, and then chain another then method to transform the data into what we need:

  onSubmitNewDate = (text) => {
    const url = `https://api.nasa.gov/neo/rest/v1/feed?api_key=DEMO_KEY&start_date=${text}`
    fetch(url).then((response) => {
      return response.json()
    }).then((raw) => {
      const asteroids = this.transform(raw)
      this.setState({
        newDate: '',
        asteroids: asteroids
      })
    })
  }

Line by line, the steps are:

  1. Build the URL for the data
  2. Start to fetch data from that URL
  3. Give a callback to execute when the data arrives
  4. Give another callback to use when the data has been converted from text to JSON (which we will look at in more detail soon).
  5. Transform that data from its raw form into the objects we need
  6. Set state

Finally, the method to transform the data NASA gives us is:

  transform = (raw) => {
    let result = []
    for (let key in raw.near_earth_objects) {
      raw.near_earth_objects[key].forEach((asteroid) => {
        result.push({
          name: asteroid.name,
          date: asteroid.close_approach_data[0].close_approach_date,
          diameter: asteroid.estimated_diameter.meters.estimated_diameter_max,
          distance: asteroid.close_approach_data[0].miss_distance.kilometers
        })
      })
    }
    return result
  }

We built this by looking at the structure of the JSON that NASA returned and figuring out how to index the fields we need. (Unfortunately, the top level of near_earth_objects is an object with dates as keys rather than an array, so we have to use let...in... rather than purely map or forEach.)

Exercises

Reset

Add a “reset” button to the counter application that always sets the counter’s value to zero. Does using it to wipe out every change you’ve made to the counter feel like a metaphor for programming in general?

Transform

Modify all of the examples after the introduction of Babel to use external scripts rather than in-pace scripts.

Validation

Modify the application so that if the starting date isn’t valid when the button is clicked, the application displays a warning message instead of fetching data.

  1. Add a field called validDate to the state and initialize it to true.
  2. Add an ErrorMessage component that displays a paragraph containing either “date OK” or “date invalid” depending on the value of validDate.
  3. Modify onSubmitNewDate so that it either fetches new data or modifies validDate.

Once you are done, search the Internet for React validation and error messages and explore other tools you could use to do this.

Key Points

  • Define event handlers to specify what actions the browser should take when the user interacts with an application.
  • The browser passes event objects containing details of events to event handlers.
  • Use classes to keep state and event handlers together.
  • React calls a class’s render to display it.
  • Separate models (which store data) from views (which display it).
  • Use fetch to get data from servers.
  • Use destructuring to get individual members from an object in a single step.
  • Modern JavaScript uses promises to manage asynchronous activities.