Reputation: 2961
I have a remote API which generates a call history list. This can of course get quite long, so I need to lazyload it. From the RN docs I gather that the best choice would be a VirtualizedList. But the documentation is terribly lacking. For example, it talks about item keys. I can provide my own function for that (using date/time value), but the getItem prop is still asking for array indexes starting with 0. So what does RN use the keys for?
Another thing is, I am printing out the calls to getItem, and renderItem, and I see a really weird pattern (I have set both initialNumToRender and maxToRenderPerBatch to 13). This is all on app startup, no user interaction. Also my getItemCount returns 15:
VirtualizedList.render() call
getItem 13 times: 0-12
getItem 0
renderItem 13 times: 0-12
VirtualizedList.render() call
getItem 13 times: 0-12
getItem 12
getItem 0
renderItem 12 times: 0-12
getItem 0
getItem 12
VirtualizedList.render() call
getItem 13 times: 0-12
getItem 12
getItem 0
renderItem 12 times: 0-12
getItem 0
getItem 12
getItem 0
getItem 12
(10 more like the 2 repeating above)
getItem 0-12
getItem 1
getItem 2
getItem 3
getItem 4
getItem 5
getItem 6
getItem 9
getItem 10
getItem 12 (Skipping some items here???)
onViewableItemsChanged, info= Object {viewableItems: Array(9), changed: Array(9)}
getItem 0-14
getItem 0-14
renderItem 0-14
onEndReached, info= Object {distanceFromEnd: 93.5} (what is that value 93.5????)
getItem 0-12
getItem 0-11
onViewableItemsChanged, info= Object {viewableItems: Array(12), changed: Array(5)}
getItem 0-14
onEndReached, info= Object {distanceFromEnd: 221???}
getItem 0-11
getItem 0-10
onViewableItemsChanged, info= Object {viewableItems: Array(11), changed: Array(1)}
getItem 0-14
Note that I haven't touched the screen yet. Now, when I scroll up a little I get the following events:
getItem 0-12
(repeats for around 20 times)
onViewableItemsChanged, info= Object {viewableItems: Array(12), changed: Array(1)}
getItem 0-12
(repeats for around 20 times)
It seems for each pixel I scroll, all items are retrieved.
For reference, here's my code:
import Expo from 'expo';
import React, { PureComponent } from 'react';
import { Platform, FlatList, VirtualizedList, View, StyleSheet, Text } from 'react-native';
import { combineReducers } from 'redux';
import { ListItem } from 'react-native-elements';
import { connect } from 'react-redux';
import I18n from '../i18n';
import { takeEvery, all, call, put, select } from 'redux-saga/effects';
import RecentRow from '../components/RecentRow';
import { getUserId } from './Settings';
import { AppText, AppHeaderText } from '../components/AppText';
// action types
const RECENT_LOAD = 'RECENT_LOAD';
const RECENT_LOAD_OK = 'RECENT_LOAD_OK';
const RECENT_LOAD_ERROR = 'RECENT_LOAD_ERROR';
// action functions
export function recentLoad(offset) {
return {
type: RECENT_LOAD,
offset: offset,
};
}
// reducers
function recent(state = { offset: 1, data: [] }, action) {
//console.log('recent', action);
switch (action.type) {
case RECENT_LOAD:
return {
...state,
offset: action.offset
};
case RECENT_LOAD_OK:
return {
...state,
data: action.data,
};
default:
return state;
}
}
// combined reducer
export const recentList = combineReducers({
recent: recent,
});
export const getRecent = state => state.recent;
export const getAccount = state => state.settings.account;
function* recentLoadData(action) {
const account = yield select(getAccount);
const URL = `https://www.xxxxx.xx/api/calls.php?userrname=${account.email}&offset=${action.offset}`;
try {
const response = yield call(fetch, URL);
if (response.status === 200) {
result = yield call([response, 'json']);
yield put({ type: RECENT_LOAD_OK, data: result });
} else {
yield put({ type: RECENT_LOAD_ERROR, error: response.status });
}
}
catch(error) {
console.log('error:', error);
yield put({ type: RECENT_LOAD_ERROR, error: error })
}
}
function* recentLoadSaga() {
yield takeEvery('RECENT_LOAD', recentLoadData);
}
export function* recentSaga() {
yield all([
recentLoadSaga(),
])
}
class RecentList extends PureComponent {
componentDidMount() {
this.props.loadRecentCalls();
}
_renderItem = (item, userid) => {
console.log('_renderItem', item);
//return <RecentRow row={item} userid={userid} />
return <ListItem title={item.item.name + ' ' + item.item.id } />
}
renderSeparator = () => {
return (
<View
style={{
height: 1,
width: "95%",
backgroundColor: "#CED0CE",
marginLeft: "5%"
}}
/>
);
};
render() {
console.log('RecentList.render()');
return (
<View style={styles.container}>
<View style={styles.lineitem}>
<View style={styles.header}>
<AppHeaderText>{I18n.t('calls')}</AppHeaderText>
</View>
</View>
<VirtualizedList
data={this.props.recent.data}
extraData={this.props}
keyExtractor={item => item.somekey}
renderItem={(item) => this._renderItem(item, this.props.userid)}
initialNumToRender="13"
maxToRenderPerBatch="13"
//ItemSeparatorComponent={this.renderSeparator}
ListEmptyComponent={ () => {
return (
<View style={styles.centerScreen}>
<View>
<AppText>{I18n.t('nocallsfound')}</AppText>
</View>
</View>
)
}}
ListFooterComponent={ () => {
return (
<Text>Footer goes here</Text>
)
}}
ListHeaderComponent={ () => {
return (
<Text>Header goes here</Text>
)
}}
getItem={ (data, index) => {
console.log('getItem', index);
return {name: 'My Name', id: index, somekey: index+1000};
}}
getItemCount={ (data, index) => {
//console.log('getItemCount');
return 15;
}}
onEndReached={ (info) => {
console.log('onEndReached, info=', info);
}}
onViewableItemsChanged={ (info) => {
console.log('onViewableItemsChanged, info=', info);
}}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
backgroundColor: 'whitesmoke',
},
header: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderColor: 'grey',
borderBottomWidth: 1,
height: 40,
},
lineitem: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
backgroundColor: 'white',
padding: 5,
},
centerScreen: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: 300,
}
});
const mapStateToProps = (state, props) => {
return {
recent: state.recentList.recent,
userid: getUserId(state),
};
};
const mapDispatchToProps = (dispatch, props) => {
return {
loadRecentCalls: () => dispatch(recentLoad(0)),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(RecentList);
So my main question is, how do I put all this together lazyloading my data?
Upvotes: 0
Views: 2023
Reputation: 2961
I solved this using redux-saga, which is so much better than redux-thunk. Here's my code, lightly edited:
Actions and reducers:
const LOAD = 'LOAD';
const LOAD_OK = 'LOAD_OK';
const LOAD_ERROR = 'LOAD_ERROR';
const REFRESH_START = 'REFRESH_START';
export function mylistRefreshStart() {
return {
type: REFRESH_START,
append: false,
};
}
export function mylistLoad() {
return {
type: LOAD,
append: true,
};
}
// reducer
export const mylist = (state = { offset: 0, limit: 50, data: [], refreshing: true }, action) => {
//console.log('mylist:', action);
switch (action.type) {
case REFRESH_START:
return {
...state,
refreshing: true,
offset: 0,
limit: 50,
};
case LOAD_OK:
return {
...state,
data: action.append ? state.data.concat(action.data) : action.data,
refreshing: false,
limit: action.data.length !== 50 ? 0 : 50,
};
case LOAD_ERROR:
return {
...state,
refreshing: false,
};
default:
return state;
}
};
// selector
export const getMyData = state => state.mylist;
Actually loading the data:
function* mylistLoadData(action) {
const mylist = yield select(getMyData);
if (mylist.limit === 0) {
//console.log('nothing left to fetch');
return;
}
try {
const response = yield call(fetch, 'https://www.example.com/api/mylist.php', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
offset: action.append ? mylist.offset + mylist.data.length : mylist.offset,
limit: mylist.limit,
}),
});
if (response.status === 200) {
result = yield call([response, 'json']);
yield put({ type: LOAD_OK, data: result, append: action.append });
} else {
yield put({ type: LOAD_ERROR, error: response.status });
}
}
catch(error) {
console.log('error:', error);
yield put({ type: LOAD_ERROR, error: error })
}
}
The saga that handles all processing:
export function* mylistSaga() {
yield takeLatest(REFRESH_START, mylistLoadData);
yield takeLatest(LOAD, mylistLoadData);
}
Rendering:
class MyList extends PureComponent {
componentDidMount = () => {
this.props.refreshStart();
};
onRefresh = () => {
this.props.refreshStart();
};
onEndReached = () => {
this.props.mylistLoad();
};
render = () => {
return (
<View style={styles.container}>
<FlatList
data={this.props.mylist.data}
extraData={this.props}
keyExtractor={item => item.id}
refreshing={this.props.recent.refreshing}
renderItem={(item) => this._renderItem(item)}
ListEmptyComponent={ () => {
if (this.props.mylist.refreshing) return null;
return (
<View style={styles.centerScreen}>
<View>
<Text>Nothing found</Text>
</View>
</View>
)
}
}
onRefresh={() => this.onRefresh()}
onEndReached={() => this.onEndReached()}
/>
</View>
);
}
}
and connecting the actions:
const mapStateToProps = (state, props) => {
return {
mylist: state.mylist,
};
};
const mapDispatchToProps = (dispatch, props) => {
return {
refreshStart: () => dispatch(recentRefreshStart()),
mylistLoad: () => dispatch(mylistLoad()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(MyList);
Basically I am just filling the data[] part in my store, and let FlatList doing the rendering of whatever is needed to be shown.
Upvotes: 3