React Data Layer - Part 6

May 28, 2019 0 Comments

React Data Layer - Part 6



This post is the sixth part of an 8-part series going in-depth into how to build a robust real-world frontend app data layer. See the previous parts here:

Using the latest browsers features to save your code and data offline can provide a better experience for your webapp’s users, but it comes with a cost. This really makes your app a distributed system, in the sense that there is both local and remote data, and they need to be kept in sync–and distributed systems are inherently complex. Even in the simplest cases, it will take more work to build the code, and more attention to ensure you don’t break things when you make changes.

In this post, we’ll walk through some of the offline features you can easily set up using Redux connecting to a RESTful web service. Then we’ll discuss options if you have more complex needs.

If you like, you can download the app as of the end of the post.

Persisting Redux Data

In part 3 we began persisting your login state by installing Redux Persist and storing a login flag. We went with Redux Persist because we knew we would be storing our entire Redux store as well.

Throttle your network connection to Slow 3G and reload the page.

slow 3G

Note that even though we’ve persisted our data, we don’t see any difference in app behavior yet: we still get a loading indicator until the web service request returns. This is because as soon as the logged-in page displays, we kick off a request to load the data from the server, and this causes the loading message to show in place of our data. Even though the video games are reloaded from persistent storage, they aren’t displayed.

To think about why this is and how we can improve it, it’s helpful to think in terms of offline patterns Jake Archibald describes in his Offline Cookbook. We’re using the terms in a bit of a different context, but they’re still helpful to talk about decisions between cached and network data.

(The Offline Cookbook talks in terms of HTTP requests sent through a Service Worker, addressing whether the Service Worker decides to respond to them from the cache or from the network. In our case, as mentioned in part 4, we aren’t using a Service Worker for cached data; instead, we’re using data cached in Redux Persist. So for us, the decision between cache and network is the decision of whether to render data already cached in the Redux store, or whether to make a network request to retrieve updated data.)

The user’s experience right now in the app could be described as “Network Only”. When they load the video game screen, if there is no network connection, they get an error. They can only see their video game data if a network requests succeeds.

What other options do we have?

Network Falling Back to Cache

One option would be to display the cached data if the network request fails. This is referred to as “Network Falling Back to Cache”.

All we have to do to implement this is to change how our LoadingIndicator works:

 const LoadingIndicator = ({ loading, error, children }) => { if (loading) { return <div> <Preloader size="small" /> </div>; 
- } else if (error) {
- return <p>An error occurred.</p>;
} else { return <div>
+ {possibleErrorMessage(error)}
{children} </div>; } }; +const possibleErrorMessage = (error) => {
+ if (error) { + return <p>An error occurred.</p>; + } +}; +
export default LoadingIndicator;

After this change, the error state is no longer a different state in our app. If the app has errored out, we simply display the error message on top of the list of records. If we didn’t have offline data this wouldn’t give us any benefit, because if the request errored we would know for sure that we wouldn’t have any data. But now we might have persisted offline data.

Run the app and log in once to make sure the video game data is downloaded. Then select the “Offline” checkbox, and click the Reload button we created. You should see the preloader briefly, then the list of video games should reappear, with an error message on top.

network falling back to cache

Cache Falling Back to Network

Another option we could have is to first try to display the cached data, and only make a network request if we don’t have any cached data. This is referred to as “Cache Falling Back to Network.”

How can we determine if we have cached data or not? We could just check that the list of video games is empty, but that could be unreliable. The list would also be empty if we deleted out all the video games. It can be more reliable to track a new state property that simply records whether we’ve loaded data before. Add a new loaded reducer to games/reducers.js:

+export function loaded(state = false, action) { 
+ switch (action.type) { + case STOREGAMES: + return true; + default: + return state; + } +}
... export default combineReducers({ games,
+ loaded,
loading, error, });

Notice how simple it is; it starts false, and we set it to true the first time we successfully load games, and then never change it again.

Now configure GameList/index.js to map it to a prop for GameList:

 function mapStateToProps(state) { return pick(, [ 'games', 
+ 'loaded',
'loading', 'error', ]); }

Now, in GameList, check the loaded flag before you call loadGames():

 const GameList = ({ games, 
+ loaded,
loading, error, loadGames, addGame, logOut, }) => { useEffect(() => {
- loadGames();
+ if (!loaded) {
+ loadGames(); + }
}, []);

Now the first time the video game list screen displays, loaded is false, and this confirms that we don’t have a cache of records. So we fall back to the network, making a loadGames() request. From here on out, loaded will be true, confirming that we do have a cache. So we don’t automatically make the network request. As a result, the LoadingIndicator will just display our records right away. Run the app to confirm this is the case.

Note that in this approach our app never automatically gets updates from the server; we have to click Refresh manually to get them. There’s another approach we can take that gives us the best of both worlds in some scenarios.

If we have cached data, we can show it right away, and also kick off a server request. If we receive data in response, we can update the data shown. This is referred to as “Cache Then Network”. This is a good default approach: for example, Ember Data, the built-in data layer for Ember.js, takes this approach for all requests by default.

To make this work from a UX perspective, we want to show our cached data while the network request is running to hopefully retrieve the new data. We can implement this by making another change to LoadingIndicator:

 const LoadingIndicator = ({ loading, error, children }) => { 
- if (loading) {
- return <div> - <Preloader size="small" /> - </div>; - } else { - return <div> - {possibleErrorMessage(error)} - {children} - </div>; - }
+ return <div>
+ {possibleLoadingMessage(loading)} + {possibleErrorMessage(error)} + {children} + </div>;
}; +const possibleLoadingMessage = (loading) => {
+ if (loading) { + return <Preloader size="small" />; + } +};

Note that we now always display our video game title list; we just optionally display a loading or error message at the top in those states.

In this approach, we will no longer need our loaded state item because we will always make a network request, so we can revert the change we previously made to GameList.js:

 const GameList = ({ games, 
- loaded,
loading, error, loadGames, addGame, logOut, }) => { useEffect(() => {
- if (!loaded) {
- loadGames(); - }
+ loadGames();
}, []);

And GameList/index.js:

 function mapStateToProps(state) { return pick(, [ 'games', 
- 'loaded',
'loading', 'error', ]);

And games/reducers.js:

-export function loaded(state = false, action) { 
- switch (action.type) { - case STORE
GAMES: - return true; - default: - return state; - } -} ... export default combineReducers({ games,
- loaded,
loading, error, });

Run the app, then check the “Offline” checkbox and click “Reload”. You should see the preloader, then the error message, but the cached data should continue to display the whole time.

cache then network error

Note that in the console you’ll see a warning from Redux Persist that an unexpected key loading was found in the persisted data, and will be ignored. When making changes to your persisted data, it’s important to keep in mind that users of your app will already have data persisted in the old format. In this case the default behavior was fine, but in more complex cases you may need to add code to detect data in the old format and migrate it to the new format.

In this post we’ve covered four major patterns for reading data:

  • Network Only
  • Network Falling Back to Cache
  • Cache Falling Back to Network
  • Cache Then Network

For your application, think through the tradeoffs of these approaches and decide which works best. You can take different approaches for different data items. For now, let’s stick with “Cache Then Network” for our video games.

With this we’ve handled reading data while offline. This is great, and for mostly-read applications it may be just what your users need. But is there anything we can do if our users need to write data while offline? There is, but it will take some work. We’ll find out how in the next part.

Click here for Part 7

Tag cloud