Components & Servers

Introduction

In the last chapter, we used a methodology to construct a React app. State management of timers takes place in the top-level component TimersDashboard. As in all React apps, data flows from the top down through the component tree to leaf components. Leaf components communicate events to state managers by calling prop-functions.

At the moment, TimersDashboard has a hard-coded initial state. Any mutations to the state will only live as long as the browser window is open. That's because all state changes are happening in-memory inside of React. We need our React app to communicate with a server. The server will be in charge of persisting the data. In this app, data persistence happens inside of a file, data.json.

EditableTimer and ToggleableTimerForm also have hard-coded initial state. But because this state is just whether or not their forms are open, we don't need to communicate these state changes to the server. We're OK with the forms starting off closed every time the app boots.

Preparation

To help you get familiar with the API for this project and working with APIs in general, we have a short section where we make requests to the API outside of React.

curl

We'll use a tool called curl to make more involved requests from the command line.

OS X users should already have curl installed.

Windows users can download and install curl here: https://curl.haxx.se/download.html.

server.js

Included in the root of your project folder is a file called server.js. This is a Node.js server specifically designed for our time-tracking app.

You don't have to know anything about Node.js or about servers in general to work with the server we've supplied. We'll provide the guidance that you need.

server.js uses the file data.json as its "store." The server will read and write to this file to persist data. You can take a look at that file to see the initial state of the store that we've provided.

server.js will return the contents of data.json when asked for all items. When notified, the server will reflect any updates, deletes, or timer stops and starts in data.json. This is how data will be persisted even if the browser is reloaded or closed.

Before we start working with the server, let's briefly cover its API. Again, don't be concerned if this outline is a bit perplexing. It will hopefully become clearer as we start writing some code.

The Server API

Our ultimate goal in this chapter is to replicate state changes on the server. We're not going to move all state management exclusively to the server. Instead, the server will maintain its state (in data.json) and React will maintain its state (in this case, within this.state in TimersDashboard). We'll demonstrate later why keeping state in both places is desirable.


`TimersDashboard` communicates with the server

If we perform an operation on the React ("client") state that we want to be persisted, then we also need to notify the server of that state change. This will keep the two states in sync. We'll consider these our "write" operations. The write operations we want to send to the server are:

  • A timer is created
  • A timer is updated
  • A timer is deleted
  • A timer is started
  • A timer is stopped

We'll have just one read operation: requesting all of the timers from the server.

HTTP APIs

This section assumes a little familiarity with HTTP APIs. If you're not familiar with HTTP APIs, you may want to read up on them at some point.

However, don't be deterred from continuing with this chapter for the time being. Essentially what we're doing is making a "call" from our browser out to a local server and conforming to a specified format.

text/html endpoint

GET /

This entire time, server.js has actually been responsible for serving the app. When your browser requests localhost:3000/, the server returns the file index.html. index.html loads in all of our JavaScript/React code.

Note that React never makes a request to the server at this path. This is just used by the browser to load the app. React only communicates with the JSON endpoints.

JSON endpoints

data.json is a JSON document. As touched on in the last chapter, JSON is a format for storing human-readable data objects. We can serialize JavaScript objects into JSON. This enables JavaScript objects to be stored in text files and transported over the network.

data.json contains an array of objects. While not strictly JavaScript, the data in this array can be readily loaded into JavaScript.

In server.js, we see lines like this:


fs.readFile(DATA_FILE, function(err, data) {
  const timers = JSON.parse(data);
  // ...
});

data is a string, the JSON. JSON.parse() converts this string into an actual JavaScript array of objects.

GET /api/timers

Returns a list of all timers.

POST /api/timers

Accepts a JSON body with title, project, and id attributes. Will insert a new timer object into its store.

POST /api/timers/start

Accepts a JSON body with the attribute id and start (a timestamp). Hunts through its store and finds the timer with the matching id. Sets its runningSince to start.

POST /api/timers/stop

Accepts a JSON body with the attribute id and stop (a timestamp). Hunts through its store and finds the timer with the matching id. Updates elapsed according to how long the timer has been running (stop - runningSince). Sets runningSince to null.

PUT /api/timers

Accepts a JSON body with the attributes id and title and/or project. Hunts through its store and finds the timer with the matching id. Updates title and/or project to new attributes.

DELETE /api/timers

Accepts a JSON body with the attribute id. Hunts through its store and deletes the timer with the matching id.

Playing with the API

If your server is not booted, make sure to boot it:


npm start

You can visit the endpoint /api/timers endpoint in your browser and see the JSON response (localhost:3000/api/timers). When you visit a new URL in your browser, your browser makes a GET request. So our browser calls GET /api/timers and the server returns all of the timers:


Note that the server stripped all of the extraneous whitespace in data.json, including newlines, to keep the payload as small as possible. Those only exist in data.json to make it human-readable.

We can use a Chrome extension like JSONView to "humanize" the raw JSON. JSONView takes these raw JSON chunks and adds back in the whitespace for readability:


Visiting the endpoint after installing JSONView

We can only easily use the browser to make GET requests. For writing data — like starting and stopping timers — we'll have to make POST, PUT, or DELETE requests. We'll use curl to play around with writing data.

Run the following command from the command line:


$ curl -X GET localhost:3000/api/timers

The -X flag specifies which HTTP method to use. It should return a response that looks a bit like this:


[{"title":"Mow the lawn","project":"House Chores","elapsed":5456099,"id":"0a4a79cb-b06d-4cb1-883d-549a1e3b66d7"},{"title":"Clear paper jam","project":"Office Chores","elapsed":1273998,"id":"a73c1d19-f32d-4aff-b470-cea4e792406a"},{"title":"Ponder origins of universe","project":"Life Chores","id":"2c43306e-5b44-4ff8-8753-33c35adbd06f","elapsed":11750,"runningSince":"1456225941911"}]

You can start one of the timers by issuing a PUT request to the /api/timers/start endpoint. We need to send along the id of one of the timers and a start timestamp:


$ curl -X POST \
-H 'Content-Type: application/json' \
-d '{"start":1456468632194,"id":"a73c1d19-f32d-4aff-b470-cea4e792406a"}' \
localhost:3000/api/timers/start

The -H flag sets a header for our HTTP request, Content-Type. We're informing the server that the body of the request is JSON.

The -d flag sets the body of our request. Inside of single-quotes '' is the JSON data.

When you press enter, curl will quickly return without any output. The server doesn't return anything on success for this endpoint. If you open up data.json, you will see that the timer you specified now has a runningSince property, set to the value we specified as start in our request.

If you'd like, you can play around with the other endpoints to get a feel for how they work. Just be sure to set the appropriate method with -X and to pass along the JSON Content-Type for the write endpoints.

We've written a small library, client, to aid you in interfacing with the API in JavaScript.

Note that the backslash \ above is only used to break the command out over multiple lines for readability. This only works on macOS and Linux. Windows users can just type it out as one long string.

Tool tip: jq

macOS and Linux users: If you want to parse and process JSON on the command line, we highly recommend the tool "jq."

You can pipe curl responses directly into jq to have the response pretty-formatted:


curl -X GET localhost:3000/api/timers | jq '.'

You can also do some powerful manipulation of JSON, like iterating over all objects in the response and returning a particular field. In this example, we extract just the id property of every object in an array:


curl -X GET localhost:3000/api/timers | jq '.[] | { id }'

You can download jq here: https://stedolan.github.io/jq/.

Loading state from the server

Right now, we set initial state in TimersDashboard by hardcoding a JavaScript object, an array of timers. Let's modify this function to load data from the server instead.

We've written the client library that your React app will use to interact with the server, client. The library is defined in public/js/client.js. We'll use it first and then take a look at how it works in the next section.

The GET /api/timers endpoint provides a list of all timers, as represented in data.json. We can use client.getTimers() to call this endpoint from our React app. We'll do this to "hydrate" the state kept by TimersDashboard.

When we call client.getTimers(), the network request is made asynchronously. The function call itself is not going to return anything useful:


// Wrong
// `getTimers()` does not return the list of timers
const timers = client.getTimers();

Instead, we can pass getTimers() a success function. getTimers() will invoke that function after it hears back from the server if the server successfully returned a result. getTimers() will invoke the function with a single argument, the list of timers returned by the server:


// Passing `getTimers()` a success function
client.getTimers((serverTimers) => (
  // do something with the array of timers, `serverTimers`
));

client.getTimers() uses the Fetch API, which we cover in the next section. For our purposes, the important thing to know is that when getTimers() is invoked, it fires off the request to the server and then returns control flow immediately. The execution of our program does not wait for the server's response. This is why getTimers() is called an asynchronous function.

The success function we pass to getTimers() is called a callback. We're saying: "When you finally hear back from the server, if it's a successful response, invoke this function." This asynchronous paradigm ensures that execution of our JavaScript is not blocked by I/O.

We'll initialize our component's state with the timers property set to a blank array. This will allow all components to mount and perform their initial render. Then, we can populate the app by making a request to the server and setting the state:


class TimersDashboard extends React.Component {
  state = {
    // leanpub-start-insert
    timers: [],
    // leanpub-end-insert
  };

  componentDidMount() {
    this.loadTimersFromServer();
    setInterval(this.loadTimersFromServer, 5000);
  }

  loadTimersFromServer = () => {
    client.getTimers((serverTimers) => (
        this.setState({ timers: serverTimers })
      )
    );
  };
  // leanpub-end-insert
  // ...

A timeline is the best medium for illustrating what happens:

  1. Before initial render

    React initializes the component. state is set to an object with the property timers, a blank array, is returned.

  2. The initial render

    React then calls render() on TimersDashboard. In order for the render to complete, EditableTimerList and ToggleableTimerForm — its two children — must be rendered.

  3. Children are rendered

    EditableTimerList has its render method called. Because it was passed a blank data array, it simply produces the following HTML output:


    <div id='timers'>
    </div>
    

    ToggleableTimerForm renders its HTML, which is the "+" button.

  4. Initial render is finished

    With its children rendered, the initial render of TimersDashboard is finished and the HTML is written to the DOM.

  5. componentDidMount is invoked

    Now that the component is mounted, componentDidMount() is called on TimersDashboard.

    This method calls loadTimersFromServer(). In turn, that function calls client.getTimers(). That will make the HTTP request to our server, requesting the list of timers. When client hears back, it invokes our success function.

    On invocation, the success function is passed one argument, serverTimers. This is the array of timers returned by the server. We then call setState(), which will trigger a new render. The new render populates our app with EditableTimer children and all of their children. The app is fully loaded and at an imperceptibly fast speed for the end user.

We also do one other interesting thing in componentDidMount. We use setInterval() to ensure loadTimersFromServer() is called every 5 seconds. While we will be doing our best to mirror state changes between client and server, this hard-refresh of state from the server will ensure our client will always be correct should it shift from the server.

The server is considered the master holder of state. Our client is a mere replica. This becomes incredibly powerful in a multi-instance scenario. If you have two instances of your app running — in two different tabs or two different computers — changes in one will be pushed to the other within five seconds.