Pull to refresh и пагинация
Стандартные паттерны мобильных приложений.
Pull to refresh
function Posts() {
const { data, isLoading, refetch, isRefetching } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <ActivityIndicator />;
return (
<FlatList
data={data}
renderItem={({ item }) => <PostCard post={item} />}
keyExtractor={(item) => item.id.toString()}
refreshing={isRefetching}
onRefresh={refetch}
/>
);
}
refreshing— индикаторonRefresh— что вызвать
Кастомный RefreshControl
import { RefreshControl } from 'react-native';
<FlatList
refreshControl={
<RefreshControl
refreshing={isRefetching}
onRefresh={refetch}
tintColor="#c1442e" // iOS
colors={['#c1442e']} // Android
/>
}
data={data}
renderItem={renderItem}
/>
Пагинация с useInfiniteQuery
import { useInfiniteQuery } from '@tanstack/react-query';
function Posts() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
refetch,
isRefetching,
} = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => api.get(`/posts?page=${pageParam}`).then((r) => r.data),
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
});
const posts = data?.pages.flatMap((p) => p.data) ?? [];
return (
<FlatList
data={posts}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <PostCard post={item} />}
refreshing={isRefetching}
onRefresh={refetch}
onEndReached={() => hasNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
ListFooterComponent={isFetchingNextPage ? <ActivityIndicator style={{ margin: 16 }} /> : null}
/>
);
}
Курсорная пагинация
Вместо номера страницы — курсор:
useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam }) => api.get(`/posts?cursor=${pageParam ?? ''}`),
initialPageParam: null as string | null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
Преимущества: не сдвигается при добавлении новых записей.
FlatList onEndReached
<FlatList
onEndReached={() => hasNextPage && !isFetchingNextPage && fetchNextPage()}
onEndReachedThreshold={0.5}
/>
Защита от повторных вызовов.
Footer
ListFooterComponent={() => {
if (isFetchingNextPage) return <ActivityIndicator style={{ padding: 16 }} />;
if (!hasNextPage && posts.length > 0) return <Text style={styles.end}>Больше нет</Text>;
return null;
}}
Empty state
ListEmptyComponent={
<View style={styles.empty}>
<Text>Пока нет постов</Text>
</View>
}
Оптимизация
<FlatList
data={posts}
keyExtractor={keyExtractor}
renderItem={renderItem}
initialNumToRender={10}
maxToRenderPerBatch={10}
windowSize={5}
removeClippedSubviews
/>
Мемоизируй renderItem и keyExtractor.
Ошибка загрузки следующей страницы
const { isFetchNextPageError, failureReason } = useInfiniteQuery({...});
ListFooterComponent={() => {
if (isFetchNextPageError) {
return (
<View style={styles.error}>
<Text>Ошибка загрузки</Text>
<Pressable onPress={() => fetchNextPage()}>
<Text>Повторить</Text>
</Pressable>
</View>
);
}
return null;
}}
Итоги
refreshing+onRefreshдля pulluseInfiniteQueryдля пагинацииgetNextPageParamдля следующейonEndReachedдля автозагрузкиListFooterComponentдля индикатора- Курсорная пагинация надёжнее