Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic / lazy approach to load pages #104

Open
ferrannp opened this issue Nov 20, 2019 · 25 comments
Open

Dynamic / lazy approach to load pages #104

ferrannp opened this issue Nov 20, 2019 · 25 comments
Labels
enhancement New feature or request

Comments

@ferrannp
Copy link
Contributor

ferrannp commented Nov 20, 2019

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: #83.

@huming-china
Copy link

I really need this feature

@alpha0010
Copy link
Contributor

alpha0010 commented Aug 24, 2020

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

@karanasthana
Copy link

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

@eomttt
Copy link

eomttt commented Sep 28, 2020

i want lazy loading anyone has another ways?

@tslater
Copy link

tslater commented Nov 11, 2020

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

@hengkx
Copy link

hengkx commented Jan 15, 2021

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

This was referenced Jan 15, 2021
@alpha0010 alpha0010 mentioned this issue Jan 26, 2021
3 tasks
@troZee
Copy link
Member

troZee commented Jun 10, 2021

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 troZee pinned this issue Jun 10, 2021
@troZee troZee changed the title Dynamic / lazy approach to load pages ☂️ [6.x-RC] Dynamic / lazy approach to load pages Jun 10, 2021
@DanijelBojcic
Copy link
Contributor

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!

@alpha0010
Copy link
Contributor

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

// Queue renders for next needed pages (if not already available).
requestAnimationFrame(() => {
this.setState((prevState) =>
this.computeRenderWindow({
buffer: this.props.buffer,
currentPage,
maxRenderWindow: this.props.maxRenderWindow,
offset: prevState.offset,
windowLength: prevState.windowLength,
})
);
});
.

@DanijelBojcic
Copy link
Contributor

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

@ghost
Copy link

ghost commented Jul 1, 2021

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

@alpha0010
Copy link
Contributor

  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.

@ghost
Copy link

ghost commented Jul 1, 2021 via email

@hengkx
Copy link

hengkx commented Jul 15, 2021

#216 (comment)

@troZee
Copy link
Member

troZee commented Jul 22, 2021

#398

@troZee troZee added the next label Jul 22, 2021
@ghost
Copy link

ghost commented Oct 4, 2021

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.

@plrdev
Copy link

plrdev commented Oct 5, 2021

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!

@TfADrama
Copy link

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!

@yepMad
Copy link

yepMad commented Mar 23, 2022

#537

@henrymoulton
Copy link

henrymoulton commented Aug 22, 2022

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.

@troZee
Copy link
Member

troZee commented Sep 22, 2022

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 troZee unpinned this issue Nov 7, 2022
@jthoward64
Copy link

jthoward64 commented Nov 9, 2022

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?

@troZee troZee pinned this issue Nov 9, 2022
@jthoward64
Copy link

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

@alpha0010
Copy link
Contributor

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.

@troZee troZee unpinned this issue Dec 20, 2022
@troZee troZee changed the title ☂️ [6.x-RC] Dynamic / lazy approach to load pages Dynamic / lazy approach to load pages Dec 20, 2022
@troZee
Copy link
Member

troZee commented Dec 20, 2022

#673

@troZee troZee removed the next label Dec 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.