react-native-pager-view
react-native-pager-view copied to clipboard
Dynamic / lazy approach to load pages
Feature Request
Why it is needed
Performance way to load a LOT of pages dynamically to create a truly dynamic swiper.
Possible implementation / Code sample
Right now I can do something like:
const [position, setPosition] = useState(0);
const onPageSelected = e => {
setPosition(e.nativeEvent.position);
};
const min = 0;
const max = position + numberOfPages;
const items = data.slice(min, max).map((item, index) => (
// If they are are not inside the range, we render null to get a better performance
<View key={item}>
{index < position + numberOfPages && index > position - numberOfPages ? (
<Item item={item} />
) : null}
</View>
));
<ViewPager
onPageSelected={onPageSelected}>
...
>
{items}
</ViewPager>
Contraints: List keeps growing while you swipe.
A better way instead of rendering null would be to slice from the beginning too:
const min = position - numberOfPages;
const max = position + numberOfPages;
However, this approach has a problem. Consider the scenario:
- Pages:
1 2 3 4and position = 2 (selected element is 3).
We slice from the beginning and we render:
- Pages
2 3 4 5but still position = 2 (selected element will be 4). <-- The problem is that if we change the children in this way, we need to adapt the position (here the position should be 1 for selected element to be still 3).
Another approach would be doing this by default natively: https://github.com/react-native-community/react-native-viewpager/issues/83.
I really need this feature
Basic js (ts) side implementation. Works well enough for my purposes. You may be able to adapt.
import React, {
forwardRef,
Ref,
useEffect,
useImperativeHandle,
useRef,
useState,
} from 'react';
import {NativeSyntheticEvent, StyleProp, View, ViewStyle} from 'react-native';
import ViewPager, {
ViewPagerOnPageSelectedEventData,
} from '@react-native-community/viewpager';
type PageSelectedEvent = NativeSyntheticEvent<ViewPagerOnPageSelectedEventData>;
export type RenderItem<T> = (info: {
item: T;
itemIndex: number;
visiblePage: number;
}) => React.ReactElement;
export type LazyViewPagerHandle = {setPage(selectedPage: number): void};
interface LazyViewPagerProps<T> {
/**
* Number of items to render before and after the current page. Default 1.
*/
buffer?: number;
data: T[];
/**
* Index of starting page.
*/
initialPage?: number;
onPageSelected?: (page: number) => void;
renderItem: RenderItem<T>;
style?: StyleProp<ViewStyle>;
}
function computeOffset(page: number, numPages: number, buffer: number) {
const windowLength = 1 + 2 * buffer;
let offset: number;
if (page <= buffer || numPages <= windowLength) {
offset = 0;
} else if (page >= 1 + numPages - windowLength) {
offset = Math.max(0, numPages - windowLength);
} else {
offset = page - buffer;
}
return offset;
}
function sleep(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function renderPage<T>(
renderItem: RenderItem<T>,
item: T,
itemIndex: number,
visiblePage: number,
buffer: number,
) {
const delta = Math.abs(itemIndex - visiblePage);
return (
<View key={itemIndex}>
{delta <= buffer ? renderItem({item, itemIndex, visiblePage}) : null}
</View>
);
}
function LazyViewPagerImpl<T>(
props: LazyViewPagerProps<T>,
ref: Ref<LazyViewPagerHandle>,
) {
// Internal buffer is larger; supports paging.
const internalBuffer = 8;
const buffer =
(props.buffer == null ? 1 : Math.max(0, props.buffer)) + internalBuffer;
// When set to `true`, forces `ViewPager` to remount.
const [isRefreshing, setIsRefreshing] = useState(false);
const [page, setPage] = useState(() =>
props.initialPage == null ? 0 : Math.max(0, props.initialPage),
);
const [offset, setOffset] = useState(() =>
computeOffset(page, props.data.length, buffer),
);
const targetOffset = useRef(offset);
const vpRef = useRef<ViewPager>(null);
const onPageSelected = (event: PageSelectedEvent) => {
if (offset === targetOffset.current) {
setPage(event.nativeEvent.position + offset);
}
};
useEffect(() => {
if (isRefreshing) {
setIsRefreshing(false);
}
}, [isRefreshing, setIsRefreshing]);
useEffect(() => {
const state = {live: true};
// Rate limit offset updates.
sleep(1100).then(() => {
if (state.live) {
targetOffset.current = computeOffset(page, props.data.length, buffer);
setOffset(targetOffset.current);
}
});
return () => {
state.live = false;
};
}, [buffer, page, props.data.length, setOffset, targetOffset]);
// Broadcast page selected event.
const clientOnPageSelected = props.onPageSelected;
useEffect(() => {
if (clientOnPageSelected != null) {
clientOnPageSelected(page);
}
}, [clientOnPageSelected, page]);
const windowLength = 1 + 2 * buffer;
useImperativeHandle(
ref,
() => ({
setPage: (selectedPage: number) => {
if (vpRef.current != null) {
const vpPage = selectedPage - offset;
if (vpPage >= 0 && vpPage < windowLength) {
// Inside render window, navigate normally.
vpRef.current.setPage(vpPage);
return;
}
}
// Remount component to navigate to `selectedPage`.
// TODO: Is there a cleaner way that does not involve forcing a
// rebuild of `ViewPager`?
const newOffset = computeOffset(
selectedPage,
props.data.length,
buffer,
);
targetOffset.current = newOffset;
setOffset(newOffset);
setPage(selectedPage);
setIsRefreshing(true);
},
}),
[
buffer,
offset,
props.data.length,
setIsRefreshing,
setOffset,
setPage,
targetOffset,
vpRef,
windowLength,
],
);
return isRefreshing ? (
<View style={props.style} />
) : (
<ViewPager
initialPage={page - offset}
ref={vpRef}
style={props.style}
onPageSelected={onPageSelected}>
{props.data
.slice(offset, offset + windowLength)
.map((item, index) =>
renderPage(
props.renderItem,
item,
offset + index,
page,
buffer - internalBuffer,
),
)}
</ViewPager>
);
}
export const LazyViewPager = forwardRef(LazyViewPagerImpl);
Also forwardRef() lacks generic support, so I used:
import React from 'react';
declare module 'react' {
// Redefine to better support generics.
function forwardRef<T, P = {}>(
render: (props: P, ref: React.Ref<T>) => React.ReactElement | null,
): (
props: React.PropsWithoutRef<P> & React.RefAttributes<T>,
) => React.ReactElement | null;
}
Hey! Any updates on this? Is this somewhere in the pipeline?
i want lazy loading anyone has another ways?
@alpha0010 Thanks for sharing your solution. I've verified it does work, with one caveat. When I prepend the list, it looses the position of the current page. If I make the buffer sufficiently large, this doesn't happen, but performance degrades. It also doesn't happen using vanilla react-native-pager. Wondering if you have any thoughts as to why this might be the case? I'm hoping for a way to rectify.
Do you have any solutions? We need this function very much. Thank you very much.
This feature has been implemented here: https://github.com/callstack/react-native-pager-view/releases/tag/v6.0.0-rc.0
Any feedback will be appreciated
I have found one improvable thing:
On Android the new page is rendered when the swipe is released and it blocks the UI thread, due to this the onPageScroll event is stunned on the last offset.
I suggest that next render should start after onPageSelected event.
Anyways thanks @alpha0010 for this feature!
I have found one improvable thing: On Android the new page is rendered when the swipe is released and it blocks the UI thread, due to this the
onPageScrollevent is stunned on the last offset. I suggest that next render should start afteronPageSelectedevent.
Do you have any recommendation how to do so? It is already wrapped with requestAnimationFrame() https://github.com/callstack/react-native-pager-view/blob/306d8997c703ee6884ed6fa2bd85db5ad3659bfc/src/LazyPagerView.tsx#L243-L254 .
Hmm... simply placing it as the last thing in the function doesn't help?
Hey! quick questions if you people can please answer,
-
What's the difference between flatlist and viewpager? I am implementing flatlist with horizontal scroll which loads data dynamically, it has a paging option and horizontal scroll as well
-
Is there anyway to preload data just like flatlist provides via threshold
- Flatlist manages a consecutive sequence of views. If these views are the same size as the screen, and scroll snapping is enabled, it will function similarly to the viewpager. However the location is a pixel offset from the top of the flatlist. This can most easily observed on device rotation (will see parts of the adjacent views, and likely end up on a different view than prior to rotate). Viewpager manages pages. Each page is a view. Events and operations work at page level; compare to flatlist where pixel level (which can be translated back to pages with a bit of math).
- Flatlist can work as a viewpager, but viewpager cannot work as a flatlist. For page level operations, viewpager will be simpler and require fewer workarounds.
- Not quite the same: set
bufferappropriately; have pages begin loading data on mount.
Hey Alpha,
Thank you for the reply.
Considering my every view has full width and height, I will stick to flatlist then.
My usecase is basically each card taking up the screen and horizontal scroll.
On Fri, Jul 2, 2021, 1:56 AM Alpha @.***> wrote:
- Flatlist manages a consecutive sequence of views. If these views are the same size as the screen, and scroll snapping is enabled, it will function similarly to the viewpager. However the location is a pixel offset from the top of the flatlist. This can most easily observed on device rotation (will see parts of the adjacent views, and likely end up on a different view than prior to rotate). Viewpager manages pages. Each page is a view. Events and operations work at page level; compare to flatlist where pixel level (which can be translated back to pages with a bit of math).
- Flatlist can work as a viewpager, but viewpager cannot work as a flatlist. For page level operations, viewpager will be simpler and require fewer workarounds.
- Not quite the same: set buffer https://github.com/callstack/react-native-pager-view/blob/next/src/types.ts#L145 appropriately; have pages begin loading data on mount.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/callstack/react-native-pager-view/issues/104#issuecomment-872528841, or unsubscribe https://github.com/notifications/unsubscribe-auth/AI36LMJSWAKUGPQ7IOSZIHDTVTFPDANCNFSM4JPRFHOQ .
https://github.com/callstack/react-native-pager-view/issues/216#issuecomment-879522761
https://github.com/callstack/react-native-pager-view/issues/398
Moved from flatlist to pagerview for my implementation, excellent performance when it comes to horizontal swipes even for large dynamic data. Thank you for this.
Hey this has been as RC release for quite some time, with many regular releases after it. Are there still some issues with this or what is the reasoning not making a non-RC release with lazy pager included? I am wondering whether to try this feature out, but I would rather wait if it is still considered not release ready. Thanks!
Hey, This version is a nice one, congratz!
I experimented on my large list of webviews (and i mean, very very large list) and it has the best behaviour among other libs i used. On prod i use the version 4.2.0 and it works nice, but this one is better.
I will have to stick with the old version because the vertical scroll from the webviews messes around with the scroll from the pager view.
But anyway, good job!
https://github.com/callstack/react-native-pager-view/issues/537
Hey this has been as RC release for quite some time, with many regular releases after it. Are there still some issues with this or what is the reasoning not making a non-RC release with lazy pager included? I am wondering whether to try this feature out, but I would rather wait if it is still considered not release ready. Thanks!
@troZee are you able to respond to this, feel like a lot of users would like to know and would help them when making a choice of v5 vs v6.
I think, we can implement JS windowing effect like this https://twitter.com/Tr0zZe/status/1572897540122574849 to achieve lazy loading.
Thanks to fabric, it would be much easier to implement and more powerful. Right now, we are focused on fabric migration, so we will return to this, once we finished fabric migration. This lazy approach will be only available for new arch.
cc @krozniata
So for those of us who cannot transition to fabric, will the 6.0.0 RCs work, or do we need to try and use FlatList instead?
EDIT: I guess my real question is, what is the most recent/most stable version/RC of lazy paging that will work with the old architecture?
So for those of us who cannot transition to fabric, will the 6.0.0 RCs work, or do we need to try and use FlatList instead?
And just for completeness sake the reason I and many others cannot transition to fabric/new architecture is the various incompatible combinations of Hermes, Expo, use_frameworks! (generally due to react-native-firebase), and flipper (at least I think it's still incompatible).
I guess my real question is, what is the most recent/most stable version/RC of lazy paging that will work with the old architecture?
I use 6.0.0-rc.2.
https://github.com/callstack/react-native-pager-view/issues/673