Decoupling views from data fetching
Imagine you're building an application for attendees of a huge conference. This app will have a schedule, speaker bios, an exhibit hall map, a searchable list of vendors, etc. Even though there are quite a few things this app can do, let's assume the home page of this app is mostly marketing content with big buttons directing the user to the schedule, speaker bios, etc. In a traditional "coupled" component architecture, that would mean you don't fetch much data at all on that homepage. It's not showing any components on that homepage that require additional data, so no API calls are made up front.
But, let's think about this. If someone is opening the conference app, there are specific pieces of data we know the user is very likely to want. So whether they're on the /schedule
page or not, we should probably fetch schedule data up front, right? In fact, we should probably fetch and cache it locally, especially since conference wifi isn't known for its reliability.
But, the point is, why wait? Just because we don't happen to be looking at the page with the schedule, doesn't mean we shouldn't fetch this data.
In some instances, it makes perfect sense to couple fetching data to the showing of a particular component. But even in those cases, it feels a bit arbitrary to automatically fetch something whenever that component happens to be visible. Let's say we had just fetched that data two seconds ago, should we really trigger another fetch? Let's say our app has a list of speakers and that tapping on a particular speaker takes you to a page with their full bio and pictures, etc. In that case, users are likely to frequently return to that list view by clicking "back" after tapping on a given speaker. In that scenario, the idea of just automatically triggering a new data fetch every time the user jumps back to the list of speakers seems silly. We should fetch our speaker data frequently enough to where it's not likely to be stale, but the speaker data isn't likely to have changed in the last couple of minutes. There's no reason not to hang on to that data and show what we have.
Additionally, in the scenario where the data is, in fact, a bit stale we can keep showing the data that we already have while updating it behind the scenes. Unless it is too stale to be useful. Ultimately, we're just checking if something has changed, so as long as the data that we have is relatively recent, we may not even need to show the user that we're updating it. In these scenarios, an alternate UI approach may be better. For example, we may want to indicate that we're refreshing their data by using a subtle global loading indicator of some sort to communicate to the user that something is happening. We may also want to add a subtle "last updated at" message somewhere in the UI if this is important for the user to know.
What about failures? If the data we have is reasonably recent, we should keep showing what we have fetched even if the attempt to refresh the data fails! In fact, I'd go one step further. If we treat a working internet connection as an enhancement, then really, we should expect it to fail and retry it automatically a bit later. Not until the data is too stale to be useful, and we've continually failed to fetch new data should we bother the user with an error message. Instead, let's keep showing what we've already fetched!
This pattern pairs nicely with the whole idea of Progressive Web Apps (PWAs). If we're building something that is indeed an application, not a simple website, we should assume it will have a life outside a working network connection. In fact, we should build them with the assumption that they're going to experience failed network requests. As a side note, if you're building a web app in this day and age, I'm of the opinion that not making it a PWA would be a mistake. See my "Betting on The Web article" in the appendix if you're curious why.
Good PWAs cache more than just the "application shell" (the code required to run the UI). They should also be caching the data fetched in the course of using the app. That way, if you open it while you're offline, it will at least show you the content it last had while trying to update.
What's your point with all this, Henrik?!
The point is: there's a strong case to be made for decoupling data fetching from whatever components happen to be showing at the time.
Decoupling these concepts can also simplify building other cool features in our app. For example, if we're fetching the conference schedule behind the scenes, we can do things like pop up a little in-app notification letting them know that the keynote is about to start, and they should find a seat! If we only think about data as being tied to display of that data, we may not have even thought of this useful feature! It would definitely have been messier to build it if triggering the fetch was linked to showing a particular component.
A real example: Starbucks
As I mentioned already, I recently spent 18 months working as an external contractor with Starbucks to help their internal development teams transition from their mostly static, server-rendered .NET platform to a new React- and Redux-based architecture. We built several different things, but it culminated in building a Progressive Web App (PWA) for the logged in user experience (a.k.a. the account dashboard). Part of impetus was to unify the user experience across platforms. So the Web Platform would be on equal footing as the iOS and Android native applications, but you wouldn't have to download an app from an app store to get this experience.
For the Starbucks app, I added a lot of pre-emptive data fetching. In case you're not a Starbucks aficionado, some of the main features of the app are the ability to track and manage your Starbucks Rewards points, and account balance. Your account balance is stored on the various Starbucks cards that you may have linked to your account. A significant percentage of users pay for their drinks at the checkout counter by opening their Starbucks app, and pulling up a barcode screen that they hold up to a scanner at the register.
We were adding the same capabilities to the web app. Here's the thing, you really don't want that barcode to fail to load when you're at the counter. If it fails, instead of rewarding the customer with a sense of being in the "cool club" and feeling like "Hey, check me out, I'm so awesome and techy!" they end up frustrated instead. A customer's feelings about their experience is a big deal! So what did we do? We fetched and cached this data no matter what you're looking at in the app. This was especially important in this case since phones don't always do the best job of handling the switch from cellular data to WiFi. Many folks come into Starbucks and connect to the WiFi, but may be temporarily stuck in a "lie-fi" situation where you can't fetch data despite appearing to be connected! This scenario could be super frustrating. So my theory was that if you had a good enough connection at the point where you loaded the PWA initially, that's probably the best time to preemptively fetch other crucial data as well. So we built the app to do just that.
As a result, you can:
- Sign in and load the app once.
- Never open the barcode screen.
- Close the app entirely.
- Switch your phone to airplane mode.
- Open the app
- Successfully pull up the barcode at the counter to pay for your drink.
Reliability FTW!
How would the ideal data fetching approach work?
- Fetch data we know we need up front and cache it.
- Retain the ability to use the presence of a particular component as a factor that helps determine if we should fetch, but still only fetch if data is stale.
- Be able to automatically re-fetch data just because it's old, without requiring any action by the user.
- Be able to automatically re-try failed data fetches behind the scenes and still show current data, as long as it's not too stale.
Sounds complicated, right? It may not be simple, but it also doesn't have to be as complicated as it seems.
I'll show an approach for doing this once I've introduced some other concepts, like higher-order-reducers.
Chapter recap
- Coupling data fetching and presence of a given component on the page is tempting, but somewhat limiting.
- In the context of a PWA, you want to make that app have a life of its own, regardless of the current internet connection. Doing this often involves explicitly managing locally cached data separate from the UI logic in your app.
- Pre-fetch data you know you'll need regardless of current components on the page. We did this at Starbucks, and it worked great.