Reputation: 9684
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.
Upvotes: 3
Views: 690
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
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