Chris Geirman
Chris Geirman

Reputation: 9684

Building a ListView with both API and AsyncStorage Data

To build my react native ListView, I need to pull data from two places, from a network APi and from AsyncStorage (like AppCache). The data from AsyncStorage may or may not be there, but it needs to return something either way (e.g. "not found")

Here's a gist of the current version, which works except for retrieving the cachedOn date (Line 47) https://gist.github.com/geirman/1901d4b1bfad42ec6d65#file-aircraftlist-js-L47, which is where I believe the secret sauce goes.

I think this is probably something any ReactJS developer could probably answer, though the example is React Native specific.

Screenshot

Upvotes: 3

Views: 690

Answers (2)

jevakallio
jevakallio

Reputation: 35940

The problem seems quite complex, because there are multiple levels of asynchronicity in play: fetching data, reading/writing the cache, and rendering list rows. In cases like this, breaking down the problem to smaller components usually helps.

I couldn't easily get the example code running, so I'm using a simplified example.

First, let's wrap the cache into a neat interface, so we don't need to think about the AsyncStorage semantics while working with it:

const aircraftCache = {
  // returns promise of cached aircraft, or null if not found
  getAircraft(aircraftId) {
    return AsyncStorage.getItem(aircraftId).then(data => (
      data ? JSON.parse(data) : null
    ));
  },

  // caches given aircraft object with a fresh cachedOn date
  // and returns a promise of the cached aircraft
  setAircraft(aircraftId, aircraft) {
    const cached = {...aircraft, cachedOn: new Date()};
    return AsyncStorage.setItem(aircraftId, JSON.stringify(cached)).then(() => cached);
  },

  // clears given aircraft from cache and return Promise<null>
  clearAircraft(aircraftId) {
    return AsyncStorage.removeItem(aircraftId).then(() => null);
  }
}

Then, let's limit the AircraftList responsibility to just displaying the list of data, loading indicator etc, and extract the row rendering to a separate component:

class AircraftList extends Component {
  static propTypes = {
    aircraft_list: PropTypes.arrayOf(PropTypes.shape({
      reg_number: PropTypes.string,
      ti_count: PropTypes.number
    }))
  }

  constructor(props) {
    super(props);
    this.ds = new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 });
    this.state = {
      dataSource: this.ds.cloneWithRows(this.props.aircraft_list),
      isLoading: false,
      showingCache: false
    };
  }

  aircraftLoaded(aircraft) {
    this.setState({isLoading: false});
    this.props.navigator.push({
      title: 'TI Lookup',
      component: TrackedItemIndex,
      passProps: {aircraft_object: aircraft}
    });
  }

  renderRow(aircraft) {
    return (
      <AircraftRow
        reg_number={aircraft.reg_number}
        ti_count={aircraft.ti_count}
        loading={() => this.setState({isLoading: true})}
        loaded={this.aircraftLoaded.bind(this)}
      />
    );
  }

  render() {
    // simplified view
    return(
      <ListView
        dataSource={this.state.dataSource}
        renderRow={this.renderRow.bind(this)}
      />
    );
  }
}

The individual row rendering, fetching and cache manipulation can then be encapsulated into AircraftRow component:

class AircraftRow extends Component {
  static propTypes = {
    reg_number: PropTypes.string,
    ti_count: PropTypes.number,
    loading: PropTypes.func,
    loaded: PropTypes.func
  }

  state = { cachedOn: null };

  constructor(props) {
    super(props);

    this.loadDetails = this.loadDetails.bind(this);
    this.clearDetails = this.clearDetails.bind(this);
    this.setCachedOn = this.setCachedOn.bind(this);
  }

  componentWillMount() {
    // when component is loaded, look up the cached details and
    // set the cachedOn timestamp into state
    aircraftCache.getAircraft(this.props.reg_number).then(this.setCachedOn);
  }

  loadDetails() {
    const id = this.props.reg_number;
    // notify parent that loading has started
    if (this.props.loading) {
      this.props.loading(id);
    }

    // fetch and cache the data
    this.fetchDetails(id)
      .then((aircraft) => {
        // notify parent that loading has finished
        if (this.props.loaded) {
          this.props.loaded(aircraft);
        }
      })
      .catch((e) => {
        console.error(e);
      });
  }

  fetchDetails(id) {
    // get details from the api, and fall back to the cached copy
    return Api.getTrackedItems(id)
      .then(aircraft => aircraftCache.setAircraft(id, aircraft))
      .then(this.setCachedOn)
      .catch(() => aircraftCache.getAircraft(id));
  }

  clearDetails() {
    // clear item from cache and update local state with null aircraft
    const id = this.props.reg_number;
    aircraftCache.clearAircraft(id).then(this.setCachedOn);
  }

  setCachedOn(aircraft) {
    // update local state (aircraft can be null)
    this.setState({ cachedOn: aircraft ? aircraft.cachedOn.toString() : null })
    return aircraft;
  }

  render() {
    // simplified view
    return (
      <View>
        <Text>{this.props.reg_number}</Text>
        <Text>{this.props.ti_count}</Text>
        <Text>{this.state.cachedOn}</Text>
        <Text onPress={this.loadDetails}>Load details</Text>
        <Text onPress={this.clearDetails}>Clear details</Text>
      </View>
    )
  }
}

For my money, this view still does too much. I would recommend looking into state management libraries such as Redux or MobX to further simplify the code - although they come with their own set of complexities, of course.

Upvotes: 3

Pranesh Ravi
Pranesh Ravi

Reputation: 19113

The simple way of doing this is via id mapping.

I can see that your response gives an unique id for each item. So, store the time-stamp based on the same ids in your local storage. When you map through the items of the result from the api get the id of the item and passe it to the getItem() of the local storage. This will return you the time for that id

const randomTime = [{
  id: 1,
  date: '05-Jun-2032 14:37:11'
}, {
  id: 2,
  date: '30-Jun-2006 00:02:27'
}, {
  id: 4,
  date: '22-Aug-1996 02:47:28'
}, {
  id: 6,
  date: '04-Jan-1991 23:27:15'
}]

const preProcessLocalStorage = () => {
  const data = JSON.parse(localStorage.getItem('date')) //read data from local storage
  const obj = {}
  data.forEach((el) => {
    obj[el.id] = el //convert into ids as keys object for better data retrieval 
  })
  return obj
}
class App extends React.Component{
  constructor(props){
    super(props)
    this.state = {
      loading: true,
    }
    this.apiData = []
    this.localData = []
    localStorage.setItem('date', JSON.stringify(randomTime)) //set Local data
  }

  componentDidMount() {
    this.localData = preProcessLocalStorage()
    $.get('https://jsonplaceholder.typicode.com/posts')
    .done((data) => {
      this.apiData = data
      this.setState({loading: false})
    })
  }

  render(){
    if(this.state.loading) return false

    const list = this.apiData.map((el) => {
      const time = this.localData[el.id] //find local data based on the api data id
      return <div>
        <h1>{el.id} - {el.title}</h1>
        <h4>{time || '-'}</h4>
      </div>
    })

    return <div>{list}</div>
  }
}

ReactDOM.render(<App/>, document.getElementById('app'))

Upvotes: 0

Related Questions