У меня есть попытка решения, которое работает для меня с реактивно-нативной базой огня, редуксом, реактивно-редуксом и редукс-толком, реактивной навигацией.
Вы можете создатьимя каталога "app" внутри вашего проекта и имя каталога "core" внутри каталога "app".Внутри ядра вы создаете имя каталога "actions" и имя каталога "redurs".
Внутри каталога redurs вы создаете вызов файла редуктора, например "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;
}
- Внутри каталога действий вы создаете имя файла "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;
}
- Внутри каталога действий снова вы создаете имя файла «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";
- Вы создаете имя каталога «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);
}
}
Чтобы использовать его в компоненте, теперь мы предполагаем, что вы хотите запросить список каналов с разбивкой на страницы по категориям типови отфильтровав его с нумерацией страниц по имени, вы создаете файл с именем «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);
Этокусок кодав проекте, который я реализовал, так что есть какой-то отсутствующий компонент или импорт ... Возьмите это как пример.
Это попытка решения, и я знаю, что есть лучший способ сделать это.Пожалуйста, я хотел бы получить совет.