Syncing data

Every app we build retrieves its data from the single source of truth — the server. React solves UI issues such as event delegation, code reuse, architecture and data binding. It leaves data syncing as the last major unsolved issue; syncing data in an easy and optimized manner is what new frameworks like Falcor and Relay attempt to solve.

Needs (and the problems they bring about)

The basic need is to ensure each component receives the data it needs to render correctly. This need hides other implications:

  1. We must be able to track and show loading statuses
  2. We must either:
    1. be able to request data on a component-by-component basis (declaratively)
    2. pass down requested data from parents to children continually
    3. If multiple components request the same data that request should be optimized (ie. only requested once)
  3. We should be able to track cache statuses within our reducer
  4. Data loading should work for universal apps rendered server side
Need 1: Loading statuses

Tracking the status of AJAX requests is the only way we can accurately show loading statuses in our UI. The simplest (and most typical) case is tracking whether an individual request is one of PENDING, SUCCESS or ERROR.

When tracking a series of events, however, things are more difficult. If we debounce a user's search to show live results we need to ensure that only the latest request is tracked. A user would expect to see search results for the current input value - not for a prior search term.

Need 2: Data loading and architecture

There are two architectural styles for loading data within your Redux app. The first is to load all data in a parent component and pass data down to children (eg. within a route or the parent scene component):

This is often the simplest method for small scenes, involving queries triggered from componentWillMount and componentWillUpdate. Pros and cons are:

  • + It eliminates request duplication as there's a single component making requests.
  • + It is conceptually easy to understand
  • - Passing data down from parent to child becomes tedious and verbose.
  • - Dependent data loading can become difficult (ie. one request depends on another request)

The second is to make each component request the data they need declaratively:

In this example, components request data separately while still being able to pass data to their children.

  • + It is easy to read, write, refactor and update these components
  • + Each component becomes fully independent and decoupled
  • - Naive implementations can duplicate requests

The trend towards declarative data loading favours this model, mainly as this is easier to work with. Newer React frameworks such as Falcor, GraphQL and Resolver also batch and dedupe requests automatically. It's also possible to implement using plain Redux actions combined with autoaction.

Need 3: Data caching

In Redux our reducers store a local copy of the server's state; that is, they cache data from the source of truth. As soon as we store the data in Redux it's possible that it is out of date.

If a component requests data we need to:

  1. Determine whether we have the data cached in our store already
  2. Determine whether the cached data is stale
  3. Make a new request for the data

Caching is a hard problem. It's often easier (but less performant) to assume that all existing data is stale and re-request data than to implement a caching layer that works well.

Need 4: Universal data loading

The data loading methods used should work the same when rendered universally on both the client and server.

Tools for syncing data