raarts
raarts

Reputation: 2961

How to implement the VirtualizedList with remote data?

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

Answers (1)

raarts
raarts

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

Related Questions