Synchronizing application data

So far in this chapter, you've learned how to detect the state of a network connection, and how to store data locally in a React Native application. Now it's time to combine these two concepts and implement an app that can detect network outages and continue to function.

The basic idea is to only make network requests when we know for sure that the device is online. If we know that it isn't, we can store any changes in state locally. Then, when we're back online, we can synchronize those stored changes with the remote API.

Let's implement a simplified React Native app that does this. The first step is implementing an abstraction that sits between the React components and the network calls that store data. We'll call this module store.js:

import { 
  NetInfo, 
  AsyncStorage, 
} from 'react-native'; 
import { Map as ImmutableMap } from 'immutable'; 
 
// Mock data that would otherwise come from a real 
// networked API endpoint. 
const fakeNetworkData = { 
  first: false, 
  second: false, 
  third: false, 
}; 
 
// We'll assume that the device isn't "connected" 
// by default. 
let connected = false; 
 
// There's nothing to sync yet... 
const unsynced = []; 
 
// Sets the given "key" and "value". The idea 
// is that application that uses this function 
// shouldn't care if the network is connected 
// or not. 
export const set = (key, value) => 
  // The returned promise resolves to true 
  // if the network is connected, false otherwise. 
  new Promise((resolve, reject) => { 
    if (connected) { 
      // We're online - make the proper request (or fake 
      // it in this case) and resolve the promise. 
      fakeNetworkData[key] = value; 
      resolve(true); 
    } else { 
      // We're offline - save the item using "AsyncStorage" 
      // and add the key to "unsynced" so that we remember 
      // to sync it when we're back online. 
      AsyncStorage 
        .setItem(key, value.toString()) 
        .then( 
          () => { 
            unsynced.push(key); 
            resolve(false); 
          }, 
          err => reject(err) 
        ); 
    } 
  }); 
 
// Gets the given key/value. The idea is that the application 
// shouldn't care whether or not there is a network connection. 
// If we're offline and the item hasn't been synced, read it 
// from local storage. 
export const get = key => 
  new Promise((resolve, reject) => { 
    if (connected) { 
      // We're online. Resolve the requested data. 
      resolve( 
        key ? 
          fakeNetworkData[key] : 
          fakeNetworkData 
      ); 
    } else if (key) { 
      // We've offline and they're asking for a specific key. 
      // We need to look it up using "AsyncStorage". 
      AsyncStorage 
        .getItem(key) 
        .then( 
          item => resolve(item), 
          err => reject(err) 
        ); 
    } else { 
      // We're offline and they're asking for all values. 
      // So we grab all keys, then all values, then we 
      // resolve a plain JS object. 
      AsyncStorage 
        .getAllKeys() 
        .then( 
          keys => AsyncStorage 
            .multiGet(keys) 
            .then( 
              items => resolve(ImmutableMap(items).toJS()), 
              err => reject(err) 
            ), 
          err => reject(err) 
        ); 
    } 
  }); 
 
// Check the network state when the module first 
// loads so that we have an accurate value for "connected". 
NetInfo.isConnected 
  .fetch() 
  .then( 
    (isConnected) => { connected = isConnected; }, 
    () => { connected = false; } 
  ); 
 
// Register a handler for when the state of the network changes. 
NetInfo.addEventListener( 
  'change', 
  (info) => { 
    // Update the "connected" state... 
    connected = [ 
      'wifi', 
      'unknown', 
    ].includes(info.toLowerCase()); 
 
    // If we're online and there's unsynced values, 
    // load them from the store, and call "set()" 
    // on each of them. 
    if (connected && unsynced.length) { 
      AsyncStorage 
        .multiGet(unsynced) 
        .then((items) => { 
          items.forEach(([key, val]) => set(key, val)); 
          unsynced.length = 0; 
        }); 
    } 
  } 
); 

This module exports two functions—set() and get(). Their job, unsurprisingly, is to set and get data, respectively. Since this is just a demonstration of how to sync between local storage and network endpoints, this module just mocks the actual network with the fakeNetworkData object.

Let's start by looking at the set() function. As you can see, it's an asynchronous function that will always return a promise that resolves to a Boolean value. If it's true, it means that we're online, and that the call over the network was successful. If it's false, it means that we're offline, and AsyncStorage was used to save the data.

The same approach is used with the get() function. It returns a promise that resolves a Boolean value that indicates the state of the network. If a key argument is provided, then the value for that key is looked up. Otherwise, all values are returned, either from the network or from AsyncStorage.

In addition to these two functions, this module does two other things. It uses NetInfo.fetch() to set the connected state. Then, it adds a listener for changes in the network state. This is how items that have been saved locally when we're offline become synced with the network when it's connected again.

Okay, now let's check out the main application that uses these functions:

import React, { Component } from 'react'; 
import { 
  AppRegistry, 
  Text, 
  View, 
  Switch, 
  NetInfo, 
} from 'react-native'; 
import { fromJS } from 'immutable'; 
 
import styles from './styles'; 
import { set, get } from './store'; 
 
// Used to provide consistent boolean values 
// for actual booleans and their string representations. 
const boolMap = { 
  true: true, 
  false: false, 
}; 
 
class SynchronizingData extends Component { 
 
  // The message state is used to indicate that 
  // the user has gone offline. The other state 
  // items are things that the user wants to change 
  // and sync. 
  state = { 
    data: fromJS({ 
      message: null, 
      first: false, 
      second: false, 
      third: false, 
    }), 
  } 
 
  // Getter for "Immutable.js" state data... 
  get data() { 
    return this.state.data; 
  } 
 
  // Setter for "Immutable.js" state data... 
  set data(data) { 
    this.setState({ data }); 
  } 
 
  // Generates a handler function bound to a given key. 
  save = key => (value) => { 
    // Calls "set()" and depending on the resolved value, 
    // sets the user message. 
    set(key, value) 
      .then( 
        (connected) => { 
          this.data = this.data 
            .set( 
              'message', 
              connected ? null : 'Saved Offline' 
            ) 
            .set(key, value); 
        }, 
        (err) => { 
          this.data = this.data.set('message', err); 
        } 
      ); 
  } 
 
  componentWillMount() { 
    // We have to call "NetInfo.fetch()" before 
    // calling "get()" to ensure that the 
    // connection state is accurate. This will 
    // get the initial state of each item. 
    NetInfo.fetch().then(() => 
      get() 
        .then( 
          (items) => { 
            this.data = this.data.merge(items); 
          }, 
          (err) => { 
            this.data = this.data.set('message', err); 
          } 
        ) 
    ); 
  } 
 
  render() { 
    // Bound methods... 
    const { save } = this; 
 
    // State... 
    const { 
      message, 
      first, 
      second, 
      third, 
    } = this.data.toJS(); 
 
    return ( 
      <View style={styles.container}> 
        <Text>{message}</Text> 
        <View> 
          <Text>First</Text> 
          <Switch 
            value={boolMap[first.toString()]} 
            onValueChange={save('first')} 
          /> 
        </View> 
        <View> 
          <Text>Second</Text> 
          <Switch 
            value={boolMap[second.toString()]} 
            onValueChange={save('second')} 
          /> 
        </View> 
        <View> 
          <Text>Third</Text> 
          <Switch 
            value={boolMap[third.toString()]} 
            onValueChange={save('third')} 
          /> 
        </View> 
      </View> 
    ); 
  } 
} 
 
AppRegistry.registerComponent( 
  'SynchronizingData', 
  () => SynchronizingData 
); 

As you can see, all we're doing here is saving the state of three checkboxes, which is easy enough, except for when you're providing the user with a seamless transition between online and offline modes. Thankfully, our set() and get() abstractions, implemented in another module, hide most of the details from the application functionality.

You will notice, however, that we need to check the state of the network in this module before we attempt to load any items. If we don't do this, then the get() function will assume that we're offline, even if the connection is fine. Here's what the app looks like:

Synchronizing application data

Note that you won't actually see the Saved Offline message until you change something in the UI.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.214.32