Performance way to load a LOT of pages dynamically to create a truly dynamic swiper.
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:
1 2 3 4 and position = 2 (selected element is 3).We slice from the beginning and we render:
2 3 4 5 but 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 theonPageScrollevent 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
buffer appropriately; 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
.
Most helpful comment
I really need this feature