react-native-pager-view icon indicating copy to clipboard operation
react-native-pager-view copied to clipboard

Dynamic / lazy approach to load pages

Open ferrannp opened this issue 5 years ago • 25 comments

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 4 and position = 2 (selected element is 3).

We slice from the beginning and we render:

  • Pages 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.

ferrannp avatar Nov 20 '19 11:11 ferrannp

I really need this feature

huming-china avatar Jan 06 '20 07:01 huming-china

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;
}

alpha0010 avatar Aug 24 '20 13:08 alpha0010

Hey! Any updates on this? Is this somewhere in the pipeline?

karanasthana avatar Sep 12 '20 07:09 karanasthana

i want lazy loading anyone has another ways?

eomttt avatar Sep 28 '20 03:09 eomttt

@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.

tslater avatar Nov 11 '20 19:11 tslater

Do you have any solutions? We need this function very much. Thank you very much.

hengkx avatar Jan 15 '21 11:01 hengkx

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

troZee avatar Jun 10 '21 12:06 troZee

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!

DanijelBojcic avatar Jun 14 '21 08:06 DanijelBojcic

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.

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 .

alpha0010 avatar Jun 14 '21 12:06 alpha0010

Hmm... simply placing it as the last thing in the function doesn't help?

DanijelBojcic avatar Jun 14 '21 12:06 DanijelBojcic

Hey! quick questions if you people can please answer,

  1. 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

  2. Is there anyway to preload data just like flatlist provides via threshold

ghost avatar Jul 01 '21 18:07 ghost

  1. 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.
  2. Not quite the same: set buffer appropriately; have pages begin loading data on mount.

alpha0010 avatar Jul 01 '21 20:07 alpha0010

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:

  1. 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.
  2. 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 .

ghost avatar Jul 01 '21 20:07 ghost

https://github.com/callstack/react-native-pager-view/issues/216#issuecomment-879522761

hengkx avatar Jul 15 '21 02:07 hengkx

https://github.com/callstack/react-native-pager-view/issues/398

troZee avatar Jul 22 '21 11:07 troZee

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.

ghost avatar Oct 04 '21 07:10 ghost

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!

plrdev avatar Oct 05 '21 08:10 plrdev

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!

TfADrama avatar Oct 13 '21 15:10 TfADrama

https://github.com/callstack/react-native-pager-view/issues/537

yepMad avatar Mar 23 '22 02:03 yepMad

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.

henrymoulton avatar Aug 22 '22 13:08 henrymoulton

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

troZee avatar Sep 22 '22 16:09 troZee

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?

jthoward64 avatar Nov 09 '22 15:11 jthoward64

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).

jthoward64 avatar Nov 15 '22 19:11 jthoward64

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.

alpha0010 avatar Nov 16 '22 14:11 alpha0010

https://github.com/callstack/react-native-pager-view/issues/673

troZee avatar Dec 20 '22 17:12 troZee