The offline experience (or, saying goodbye to imperative data fetching)

Dario Gieselaar
Oct 5, 2015 · 6 min read

We’ve recently had the pleasure of being able to start from scratch on our (Angular) web app, Zaaksysteem.nl. It allowed us to completely rethink some vital parts of our application as we finally transitioned from a server-side application to a client-side app. One obvious boon was offline support.

After a couple of weeks of brooding, grunting and grinding, this is the end result.

As you can see, even without a connection, the application is available. Additionally, any data, or data changes are immediately displayed in all tabs. Allow me to explain how we managed to o this.

Caching assets with ServiceWorker

With the advent of ServiceWorker, caching assets becomes a breeze, removing the need to hit the server for assets, and adding support for automatic updates. I don’t necessarily care about offline usage, but optimizing for offline brings obvious benefits to users with poor connections, and even if you have a superfast 4G connection, it’s never as fast as just getting the assets from the cache and checking for updates in the background.

sw-precache makes 90% of this ridiculously easy. Pretty much all you need to do is add a couple of lines at the end of your build file, and this nifty little library takes care of the rest: your assets are now downloaded on first run, and subsequent page loads use the cached assets. I’ve written a little service around ServiceWorker which allows us to notify the user when the assets are updated and give them the option to refresh the page.

Dealing with XHR requests

That approach partly breaks down when you’re dealing with XHR requests. You don’t really want having to prefetch all data, or have the user to look at stale data all the time, or have the user refresh every time new data is fetched in the background. That kind of defeats the purpose of a client-side app, no?

But, it only partly breaks down, because the approach of displaying cached data, and having it fetch updates in the background is pretty solid. We just need to figure a way to have it update without the user having to refresh anything.

I. Caching API data when requested

As the user navigates the application, we cache as much data in LocalStorage as possible, popping out the oldest requests if a (size) limit has been reached. This ensures that data which the users needs the most is pretty much always available to serve from cache.

II. Use a continuously updating resource instead of imperative, promise-based data fetching

We dropped $http in favor of a resource, inspired by Angular’s $resource service. A resource works in the following way:

  • The consumer passes a URL, a request object, or a function which when called returns either (or null).
  • The resource checks the cache, and if available, immediately exposes this data via a data() method.
  • If necessary, the data is fetched on the background, and when fetched, the cache is updated, and returned by data().
  • When a function is used, the resource checks the result on every digest. If the resulting URL changes, the process restarts.
  • When the cache is updated, the resource is as well. That also means data is shared across tabs, allowing us to minimize requests and preventing that the user looks at stale data.

Here’s how a $http-based controller would look vs a resource-based controller:

That, to me, is a pretty big difference — and the $http version doesn’t even include fetching data from the cache.

Resources also allow you to add reducers to it, which are called when data() is. The result of this, erm, reduction is then returned. A resource is also stateless by default, and reducers are only called when the data changes. This also means you have to safeguard against outside sources mutating the data, so we use the wonderful seamless-immutable to prevent any changes. Here’s an example:

Besides obvious performance benefits, the caching is especially useful for Angular, because ng-repeat currently can not deal with continuously changing arrays. This principle fits very well with Angular, as it just continously calls data() on every digest, freeing you from writing event boilerplate.

III. Optimistic updates & mutations as serializable objects

Not only do we want to give the user instant feedback about his action, we want the action itself to feel instant. We’ve had issues with API performance, so we need a mechanism which simulates the server response while it’s not available yet. We also wanted to make sure that they can be stored offline and synced when a connection becomes available. Also, if a tab with the application closes, or crashes, the stored mutations need to be picked up by another instance of the application running in a different tab, to ensure the user doesn’t lose any data.

To achieve this, we use mutations. A mutation simply consists of a type parameter, and a data property. They’re added to a resource via `mutate()`, which then takes care of passing the mutation to the mutationService, tacking a request on it. The mutations are then stored in LocalStorage. As soon as a mutation is added, they’re flushed, meaning they’re sent to the server. At that point (and at an interval) it also checks (via window.postMessage) for any orphaned mutations.

When a mutation is synced, an action is requested for that type. These action handlers are registered as soon as the application is initialized. An application consists of the following properties and methods:

  • type: the type of mutations the action should handle.
  • request( mutationData ): this is called by the mutationService with the mutation data. It allows the action to return a requestOptions object based on the action type and mutation data. For instance, this could be `/api/user/tweet/${mutationData.tweetId}/delete`.
  • reduce( data, mutationData ): This function is called by the resource. It should modify the stored data based on the mutationData to simulate the response of the server. It is called before any other reducer that the consumer might have added.
  • success()/error(): Optional success/error handlers.

As soon as the mutation is successfully sent to the server (or if it fails), it is removed from the resource, and the data from the resource is updated. We currently try to have mutating requests return the collection or object it was based on, but it’s pretty easy to tell the resource to fetch its data again after the mutation is completed. Because the mutations are stored in localStorage, this data can be shared across tabs as well, and any change the user makes is immediately visible in other tabs as well, without any data being fetched, as you can see in the video.

IV. Composed reducers

Sometimes, the output of your controllers method is dependent on more than one API call, or component state, or both. That’s why we use composed reducers.

You can combine multiple resources, functions or constants into one resource, and reduce it to a single value, again by calling reduce() with a reducer function. This function is then called when any of the resources updates, or the result of a function changes after a $digest. By default it only calls the given reducer method when all dependencies are resolved, but you can also have it be called on any change. Here’s what it would look like when you’re filtering a list based on user input:

We’ve just finished our first view which uses this approach, a dashboard. Here’s a real world example of a component. It’s pretty complex, with lots of dependencies, but I still feel in control. We’ll definitely explore this concept further. And we would love to hear your feedback! Especially if you know of any library we can use which does the same things described here. In any case, when it’s a little more mature (the API is hugely volatile at the moment), and if there’s interest, we’ll definitely spin it off into a standalone library.

Dario Gieselaar

Written by

JavaScript @ zoover.nl

More From Medium

More from Dario Gieselaar

Also tagged JavaScript

More from Dario Gieselaar

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade