简体   繁体   English

在Firestore中使用本机滚动到底部时如何获取数据?

[英]How to fetch data when scroll to bottom in react native with firestore?

I'm using react-native with redux , redux-thunk and firebase/firestore . 我在reduxredux-thunkfirebase/firestore使用react-native。

I wonder how to make pagination with firestore. 我想知道如何使用Firestore进行分页。 I know it's written in document but I couldn't figure it out. 我知道它写在文档中,但我无法弄清楚。

Now, I'm trying to make that whenever scroll down and it reaches bottom then fetch next data. 现在,我试图使它向下滚动并到达底部然后获取下一个数据。

example code is below. 示例代码如下。

var first = db.collection("cities")
        .orderBy("population")
        .limit(25);

    return first.get().then(function (documentSnapshots) {
  // Get the last visible document
  var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
  console.log("last", lastVisible);

  // Construct a new query starting at this document,
  // get the next 25 cities.
  var next = db.collection("cities")
          .orderBy("population")
          .startAfter(lastVisible)
          .limit(25);
});

I'm using react-redux, so I need to change this code. 我正在使用react-redux,所以我需要更改此代码。 So I guess I need to store the lastVisible as state. 所以我想我需要将lastVisible存储为状态。 But then How can I call this event? 但是那我怎么称呼这个事件呢?

If you are using a FlatList there is a property onScrollEndDrag that will fire whenever you hit the bottom of the component. 如果您使用的是FlatList有一个属性onScrollEndDrag每当你打组件的底部,这将触发。 Example: 例:

  <FlatList
       onScrollEndDrag={this.updatePageNumberAndMakeNewRequest}
      />

I have an attempt of solution that work for me with react-native-firebase, redux, react-redux and redux-thunk, react-navigation. 我尝试使用react-native-firebase,redux,react-redux和redux-thunk,react-navigation对我有用的解决方案。

  • You can create a directory name "app" inside your project and a directory name "core" inside "app" directory. 您可以在项目内部创建目录名称“ app”,并在“ app”目录内部创建目录名称“ core”。 Inside core, you create a directory name "actions" and a directory name "reducers". 在核心内部,创建目录名称“ actions”和目录名称“ reducers”。

  • Inside reducers directory, you create a reducer file call like "firestorePaginatorReducer.js" with this content below : 在reducers目录中,您创建一个reducer文件调用,例如“ firestorePaginatorReducer.js”,其内容如下:

import { 
    FIRESTORE_PAGINATOR_LIST, 
    FIRESTORE_PAGINATOR_INIT, 
    FIRESTORE_PAGINATOR_ERROR,
    FIRESTORE_PAGINATOR_RESET 
} from '../actions/types';
import { isEmpty, isArray } from 'lodash';
import { PAGINATION_ITEM_PER_PAGE } from '../../utils/firebase';

const initialState = {};

const init = { 
    data: [],
    page: 1,
    willPaginate: false,
    pageKey: null,
    unsubscribes: [],
    isLoaded: false,
    isError: false,
    isEmpty: true 
};

export default function (state = initialState, action) {
    const value = action.value;
    const key = value ? value.key : null;

    let newState;

    switch (action.type) {
        case FIRESTORE_PAGINATOR_INIT:   
            if(state.hasOwnProperty(key)) {
                newState = {
                    ...state,
                    [key]: {
                        ...state[key],
                        ...init
                    }
                };
            } else {       
                newState = {
                    ...state,
                    [key]: {...init}
                };
            }
            break;

        case FIRESTORE_PAGINATOR_LIST:
            const { data, unsubscribe, isPagination, paginationField } = value;
            const dataLength = state.hasOwnProperty(key) ? state[key].data.length + data.length :
                                data.length;
            const pageNumber = (!isPagination || dataLength === 0) ? 1 :
                                Math.ceil(dataLength / PAGINATION_ITEM_PER_PAGE);
            const willPaginate = dataLength >= pageNumber * PAGINATION_ITEM_PER_PAGE

            newState = {
                ...state,
                [key]: {
                    ...state[key],
                    page: pageNumber,
                    willPaginate: willPaginate,
                    data: !isPagination ? [].concat(data) : state[key].data.concat(data),                
                    pageKey: isArray(data) && data.length > 0 ? data[data.length - 1][paginationField] : null,
                    unsubscribes: state.hasOwnProperty(key) ?
                        [ ...state[key].unsubscribes, unsubscribe ] : [].push(unsubscribe), 
                    isLoaded: true,
                    isError: false,
                    isEmpty: state.hasOwnProperty(key) ? 
                        isEmpty(state[key].data) && isEmpty(data) : isEmpty(data)
                }
            };
            break;

        case FIRESTORE_PAGINATOR_ERROR:
            newState = {
                ...state,
                [key]: {
                    ...state[key],
                    isLoaded: true,
                    isError: true,
                    isEmpty: state.hasOwnProperty(key) ? 
                        isEmpty(state[key].data) : true
                }
            };  
            break;

        case FIRESTORE_PAGINATOR_RESET:
            newState = {
                ...state,
                [key]: {
                    ...state[key],
                    ...init
                }
            };  
            break;

        default:
            newState = state;
            break;
    }

    return newState || state;

}
  • Inside actions directory, you create a file name "actions.js" with this content below 在动作目录中,使用以下内容创建文件名“ actions.js”
import { 
    FIRESTORE_PAGINATOR_LIST, 
    FIRESTORE_PAGINATOR_INIT, 
    FIRESTORE_PAGINATOR_ERROR, 
    FIRESTORE_PAGINATOR_RESET
} from './types';
import { paginate } from '../../utils/firebase';
import { isArray } from 'lodash';

export const setPaginationListener = (
    query, 
    paginationField, 
    pageKey = null, 
    isPagination = false,
    sort = "DESC"
) => (dispatch, getState)=> {

    if(!isPagination){
        unsetListeners(query, dispatch, getState);

        dispatch({
            type: FIRESTORE_PAGINATOR_INIT,
            value: {
                key: query.storeAs
            }
        });
    } 

    return new Promise(resolve => setTimeout(resolve, 1000)).then(() => {
        const unsubscribe = paginate(
            (querySnapShot) => {
                let data = [];    
                querySnapShot.docs.forEach((snap) => {
                    const doc = snap.data();             
                    data.push({ ...doc, id: snap.id });
                });

                dispatch({
                    type: FIRESTORE_PAGINATOR_LIST,
                    value: {
                        key: query.storeAs,
                        data: data,
                        unsubscribe: unsubscribe,
                        isPagination: isPagination,
                        paginationField: paginationField
                    }
                });
            },
            (error) => { 
                dispatch({ 
                    type: FIRESTORE_PAGINATOR_ERROR,
                    value: {
                        key: query.storeAs
                    }
                });
            },
            createPathFromQuery(query),
            isArray(query.orderBy) ? query.orderBy : paginationField,
            query.where ? query.where : null,
            pageKey,
            query.endAt,
            sort
        );
    }); 
}    

export const unsetPaginationListener = (query) => (dispatch, getState) => {

    unsetListeners(query, dispatch, getState);

    return;
}

export function getStateRequest(data) {
    if(typeof data === "object" && 
        data.hasOwnProperty("isLoaded") && 
        data.hasOwnProperty("isError") &&
        data.hasOwnProperty("isEmpty")
    ) {
        return {
            isLoaded: data.isLoaded, 
            isError: data.isError, 
            isEmpty: data.isEmpty
        };
    } 

    return {
        isLoaded: false, 
        isError: false, 
        isEmpty: true
    };
}

function unsetListeners(query, dispatch, getState) {
    const key = query.storeAs;
    const state = getState().firestorePaginator;
    const unsubscribes = state.hasOwnProperty(key) ? state[key].unsubscribes : null;

    if(isArray(unsubscribes) && unsubscribes.length > 0) {
        unsubscribes.forEach((unsubscribe) => {
            unsubscribe();
        });

        dispatch({
            type: FIRESTORE_PAGINATOR_RESET,
            value: {
                key: key
            }
        });
    }
}

function createPathFromQuery(query) {
    const collection = query.collection;
    const doc = query.doc;
    const subcollections = query.subcollections;

    if(collection) {
        if(doc) {
            if(subcollections && isArray(subcollections)) {
                return collection + "/" + doc + "/" + subcollections[0].collection;
            } else {
                return collection + "/" + doc;
            }
        } else { 
            return collection;
        }
    }

    return null;
}
  • Inside actions directory again, you create a file name "types.js" with the content below 再次在actions目录内,创建文件名称为“ types.js”,内容如下
// Manage Firestore pagination queries
export const FIRESTORE_PAGINATOR_LIST = "FIRESTORE_PAGINATOR_LIST";
export const FIRESTORE_PAGINATOR_INIT = "FIRESTORE_PAGINATOR_INIT";
export const FIRESTORE_PAGINATOR_ERROR = "FIRESTORE_PAGINATOR_ERROR";
export const FIRESTORE_PAGINATOR_RESET = "FIRESTORE_PAGINATOR_RESET";
  • You create a directory name "utils" inside app directory, then inside you create a file call "firebase.js" with this content : 您在应用程序目录中创建目录名称“ utils”,然后在其中创建具有以下内容的文件调用“ firebase.js”:
import firebase from 'react-native-firebase';
import { isArray, isString, isEmpty } from 'lodash';

export function getCurrentUserId() {
    return firebase.auth().currentUser.uid;
}

export function getCollection(collection) {
    return firebase.firestore().collection(collection);
}

export function getDoc(collection, id) {
    return getCollection(collection).doc(id);
}

// Create query according to react redux firebase
export function createQuery(
    collection,
    doc = null, 
    subcollections = null,
    orderBy = null, 
    where = null, 
    startAt = null,
    endAt = null,
    limit = null,
    storeAs = null
) {

    const query = {
        collection: collection
    };

    if(doc !== null) {
        query.doc = doc;
    }

    if(subcollections !== null) {
        query.subcollections = subcollections;
    }

    if(orderBy !== null) {
        query.orderBy = orderBy;
    }

    if(where !== null) {
        query.where = where;
    }

    if(startAt !== null) {
        query.startAfter = startAt;
    }

    if(endAt !== null) {
        query.endAt = endAt;
    }

    if(limit !== null) {
        query.limit = limit;
    }

    if(storeAs !== null) {
        query.storeAs = storeAs;
    }

    return query;

}

export const PAGINATION_ITEM_PER_PAGE = 30;

// Firestore paginator
export function paginate(
    callBackSuccess, 
    callBackError,
    collection, 
    orderByPath, 
    whereCond = null, 
    startAt = null, 
    endAt = null,
    sort = "DESC", 
    limit = PAGINATION_ITEM_PER_PAGE
){

    const collectionRef = getCollection(collection); 
    let query = collectionRef;

    if(isArray(whereCond)){
        for(let i = 0; i < whereCond.length; i++) {
            query = query.where(whereCond[i][0], whereCond[i][1], whereCond[i][2]);
        } 
    } 

    if(isString(orderByPath)) {
        query = query.orderBy(orderByPath, sort);
    } else if(isArray(orderByPath)) {
        for(let i = 0; i < orderByPath.length; i++) {
            query = query.orderBy(orderByPath[i][0], orderByPath[i][1]);
        }
    }

    if(startAt !== null && endAt !== null){

        return query.startAfter(startAt).endAt(endAt)
                    .limit(limit).onSnapshot(callBackSuccess, callBackError);

    } else if(startAt === null && endAt !== null) {

        return query.endAt(endAt).limit(limit).onSnapshot(callBackSuccess, callBackError);

    } else if(startAt !== null && endAt === null) {

        return query.startAfter(startAt).limit(limit).onSnapshot(callBackSuccess, callBackError);

    } else {

        return query.limit(limit).onSnapshot(callBackSuccess, callBackError);

    }

}

To use it in a component, now we assume that you want to query a feed list with pagination by type category and filter it with pagination by their name, you create a file call "ListFeed.js" (this assume that you already navigate to this screen from screen category) with content something like : 要在组件中使用它,现在我们假设您要按类型类别查询带有分页的提要列表,并按名称对其进行分页过滤,您将创建一个名为“ ListFeed.js”的文件(假设您已经导航到此屏幕来自屏幕类别),其内容类似于:

import React, { Component } from 'react';
import { StyleSheet, View, FlatList } from 'react-native';
import { Input, Icon, Image, Text } from 'react-native-elements';
import { connect } from 'react-redux';
import { setPaginationListener, unsetPaginationListener, getStateRequest } from '../../../core/actions/actions';
import { createQuery, PAGINATION_ITEM_PER_PAGE } from '../../../utils/firebase';
import Spinner from '../../../core/layout/Spinner';
import { isEmpty } from 'lodash';

class ListFeed extends Component {

    constructor(props) {
        super(props);

        this.state = {
            search: ''
        };

        this.query = null;
    }

    componentDidMount() {
        this._loadData();
    }

    componentWillUnmount() {
        const { unsetPaginationListener } = this.props;
        if(this.query) {
            unsetPaginationListener(this.query);
        }
    }

    _loadData(search = '', isPagination = false) {
        const { navigation, setPaginationListener, feedsByCategoryState } = this.props;
        const pageKey = feedsByCategoryState ? (isPagination ? 
            feedsByCategoryState.pageKey : null) : null;
        const { category } = navigation.state.params;
        let where = []; let endAt = null;

        if(!isEmpty(search)) {
            where.push(["name", ">=", search.toUpperCase()]);
            endAt = search.toUpperCase() + "\uf8ff";
        } 

        where.push(["type", "==", category]);

        this.query = createQuery(
            "feeds", // Feeds path in firebase
            null, 
            null, 
            null,
            where, 
            pageKey, 
            endAt, 
            PAGINATION_ITEM_PER_PAGE, 
            "feedsByCategory"
        );

        setPaginationListener(this.query, "name", pageKey, isPagination, "ASC");

    }

    _displayFeedDetails = (feedId) => {
        const { navigation } = this.props;
        navigation.navigate("feedDetails", {"feedId": feedId});
    }

    _handleInput = (text) => {
        this.setState({ search: text });
    }

    _handleSubmitSearch() {
        const { search } = this.state;
        if(!isEmpty(search)) {
            this._loadData(search, false);
        }
    }

    _handleRefreshSearch() {
        this.setState({ search: '' }, () => {
            this._loadData();
        });
    }

    _renderItem = ({ item }) => (
        <FeedItem 
            name={item.name}  
            feedId={item.id}
            displayFeedDetails={this._displayFeedDetails}
        />
    );

    _displayFeedsByCategory(feedsByCategoryState, isLoaded, isError, isEmpty) {

        if(!isLoaded) {
            return (
                <View style={{alignItems: 'center', marginTop: 50}}>
                    <Spinner isAbsolute={false} 
                        containerStyle={{marginHorizontal: 5}} 
                    />
                </View>
            );
        } else if(isError) {
            return (
                <View style={{justifyContent: 'center', alignItems: 'center', marginTop: 50}}>
                    <Text style={{color: 'white', fontWeight: 'bold'}}>
                    Erreur de connexion.
                    </Text>
                </View>
            );
        } else if(isEmpty) {
            return (
                <View style={{justifyContent: 'center', alignItems: 'center', marginTop: 50}}>
                    <Text style={{color: 'white', fontWeight: 'bold'}}>
                    Aucun point de vente trouvé.
                    </Text>
                </View>
            );
        } else {

            let feedsByCategory = [];
            let willPaginate = false;

            if(feedsByCategoryState) {
                feedsByCategory = feedsByCategoryState.data;
                willPaginate = feedsByCategoryState.willPaginate;
            }

            return (
                <FlatList
                    contentContainerStyle={styles.list_container}
                    keyExtractor={(item, index) => index.toString()}
                    data={feedsByCategory}
                    renderItem={this._renderItem}
                    onEndReachedThreshold={0.5}
                    onEndReached={() => {
                        if(willPaginate) {
                            this._loadData(this.state.search, true); 
                        }
                    }}
                />
            );
        }

    }

    render() {
        const { navigation, feedsByCategoryState } = this.props;
        const { isLoaded, isError, isEmpty } = getStateRequest(feedsByCategoryState);
        const { image } = navigation.state.params;
        const { search } = this.state;

        return (
            <View style={styles.main_container}>
                <View style={styles.search_wrapper}>
                    <Input
                        inputContainerStyle={{borderBottomWidth: 0, paddingHorizontal: 0}}
                        containerStyle={styles.search_container} 
                        onChangeText={this._handleInput}
                        value={search}
                        leftIcon={
                            <Icon iconStyle={{color: '#D20000'}}
                                containerStyle={{padding: 0}} 
                                type="font-awesome" 
                                name="search-plus" onPress={() => this._handleSubmitSearch()} 
                            />
                        } 
                        rightIcon={
                            <Icon iconStyle={{color: '#D20000'}} 
                                containerStyle={{padding: 0}}
                                type="font-awesome" 
                                name="refresh" onPress={() => this._handleRefreshSearch()} 
                            />
                        }
                        placeholder="Rechercher ..." 
                    />
                    <Image 
                        source={{uri: image}}
                        containerStyle={styles.image_container_style}
                        style={styles.image_style}
                        onError={(error) => {}}
                    />
                </View>

                />
                {
                    this._displayFeedsByCategory(
                        feedsByCategoryState,
                        isLoaded, 
                        isError, 
                        isEmpty
                    )
                }
            </View>
        )
    }
}

const styles = StyleSheet.create({
    main_container: {
        flex: 1,
        paddingBottom: 5,
        backgroundColor: '#D20000'
    },
    search_wrapper: { 
        flexDirection: 'row', 
        alignItems: 'center', 
        marginTop: 0, 
        marginBottom: 0,
        marginHorizontal: 0,
        paddingVertical: "3%",
        backgroundColor: 'white'
    },
    search_container: {
        height: 55,
        borderRadius: 10, 
        borderColor: "#D20000",
        borderWidth: 1,
        marginLeft: "1%", 
        flex: 1
    },
    title_icon_style: {
        fontSize: 13,
        color: "#D20000",
        fontWeight: 'bold'
    },
    title_icon_container_style: {
        marginRight: "1%", 
        flex: 1
    },
    list_container: {
        marginHorizontal: 5,
        marginVertical: 5,
        paddingBottom: 5,
    },
    image_container_style: {
        height: 100,
        width: 100,
        alignItems: "center", 
        backgroundColor: "white"
    },
    image_style: {
        height: 100,
        width: 100
    }
});

const mapStateToProps = (state) => ({
    feedsByCategoryState: state.firestorePaginator.feedsByCategory,
});

export default connect(mapStateToProps, { setPaginationListener, unsetPaginationListener })
(ListFeed);

This is a piece of code in a project that i implemented, so there is some missing component or import... Take it like an example. 这是我实现的项目中的一段代码,因此缺少一些组件或导入...以示例为例。

This is an attempt of solution and i know that there is a better way to do it. 这是解决方案的尝试,我知道有更好的方法来解决。 Please i would like some advice. 请给我一些建议。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM