JavaScript State Flow

This page explains how application state is managed in JavaScript.

Prerequisites

You should be familiar with (or at least know of):

Some familiarity with the Fluxtream UI also helps, since this page frequently refers to various UI elements. For a more comprehensive guide to frontend development, see Front-end Architecture.

Where Does State Come From?

There are several ways in which application state can be created/modified:

  • The user loads a valid app URL (e.g. /app/calendar/clock/date/1970-01-01) in their browser.
  • The user loads an invalid or incomplete app URL (e.g. /app) in their browser.
  • The user clicks some navigation button (e.g. "Week" in Calendar) that updates the current tab.
  • The user interacts with a widget (e.g. Grapher) that generates state updates.
  • The user changes to a different tab.
  • The user changes to a different app, loading that app's state as stored in FlxState.

Our state flow must handle all of these cases.

State Flow

Overview

FlxState.router.navigate() is the state flow entry point. It changes the URL, triggering one of the FlxState.router.route() functions set up in App.setupURLRouting(). These routes chop off the "app/{name}" prefix and call App.render(), which passes the rest of the path to the correct app. The app then parses that path into a state object. This state object is passed to the app's renderState() function, which decides how to render the state represented by that object.

In summary:

  • To change state, call FlxState.router.navigate().
  • The new URL is picked up by one of the FlxState.router.route() functions.
  • That routing function calls App.render().
  • App.render() passes state to the correct app.
  • That app parses the state into a state object, which is passed to renderState().
  • renderState() does some magic, and the user sees pretty things.

To make building those FlxState.router.navigate() calls easier, you can use Application.navigateState(). This helper function will automatically add the "app/{name}" part of the URL, and will also set up the correct navigate() parameters to ensure that Backbone routing works properly.

Example

Let's use the "Week" button in Calendar as an example. Suppose you're viewing the Clock tab. When you click that button:

  • The button $.click() handler calls fetchState().
  • fetchState() makes a $.ajax() request to /api/calendar/nav/getWeek to fetch Calendar's next state, /week/2012/50.
  • The $.ajax() success callback retrieves that state and calls Application.navigateState(state) to change the URL to /app/calendar/clock/week/2012/50.
  • The new URL is tested against the routes set up in App.setupURLRouting(), resulting in a match against the "app" route.
  • The "app" route calls parseState() to parse the URL state /clock/week/2012/50 into a state object, obj.
  • obj is passed to renderState(), which decides what to render. In this case, it needs to call fetchCalendar() for a digest of the data in that time range.
  • fetchCalendar() makes a $.ajax() request to /api/calendar/all/week/2012/50.
  • The $.ajax() success callback retrieves the Calendar data digest and passes it to the appropriate tab for rendering.

Specific Cases

Switching Between Apps

When you switch apps, App.render() notices that the app has changed and:

  • stores the old app's state in FlxState;
  • hides the old app;
  • fetches the new app's state from FlxState;
  • shows the new app.

If the new app has no state in FlxState, App.renderDefault() is called. This ultimately calls renderDefaultState() on the new app. renderDefaultState() should then call Application.navigateState() to load a default state.

Loading URLs In The Browser

Backbone.history.start() causes the initial URL to be processed via the above flow. The only difference between this and the overview above is that no explicit call to FlxState.router.navigate() is needed.

In the Calendar flow above, however, this flow skips the fetchState() steps and goes directly to the routing functions. Calendar.renderState() detects this case and performs the necessary #currentTimespanLabel setup.

Continuous Navigation

Calendar's TimelineTab is somewhat different: it allows for continuous drag-based navigation on Grapher's date axis. When the user drags the date axis, Grapher itself is already rendering the updated time range; no renderState() call is needed.

However, we would like the URL, #currentTimespanLabel, the timeUnit buttons, etc. to reflect these updates. To manage this, TimelineTab listens for date axis changes via Grapher.addAxisChangeListener(). The listener function computes the URL state closest to what is being viewed, then passes that state to Calendar.changeTabState(). Calendar.changeTabState() calls FlxState.router.navigate() to update the URL, but does not trigger the routing functions. It also updates the rest of the Calendar UI.