Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ For more examples, see the [example app](example/src): nested pagers, keyboard h
| `layoutDirection: 'ltr' \| 'rtl' \| 'locale'` | Layout direction. Use `ltr` or `rtl` to set it explicitly, or `locale` to follow `I18nManager.isRTL`. Defaults to `locale`. | both |
| `pageMargin: number` | Blank space between pages. | both |
| `keyboardDismissMode: 'none' \| 'on-drag'` | Whether the keyboard is dismissed when the user drags the pager. Defaults to `none`. | both |
| `lazy: boolean` | Mounts a page's content only when the pager gets near it. See [Lazy pages](#lazy-pages). Defaults to `false`. | both |
| `lazyPreloadDistance: number` | With `lazy`, how many pages on each side of the current page are mounted ahead of time. Defaults to `1`. | both |
| `renderLazyPlaceholder: (index: number) => ReactNode` | With `lazy`, what a page shows before its content is mounted. | both |
| `onPageScroll: (e: PagerViewOnPageScrollEvent) => void` | Called while moving between pages, either from a swipe or from an animated `setPage`. `e.nativeEvent` contains `position` and `offset`. | both |
| `onPageSelected: (e: PagerViewOnPageSelectedEvent) => void` | Called when a new page becomes selected, and once on mount for `initialPage`. `e.nativeEvent.position` is the selected index. | both |
| `onPageScrollStateChanged: (e: PageScrollStateChangedNativeEvent) => void` | Called when the scroll state changes. `e.nativeEvent.pageScrollState` is `idle`, `dragging` or `settling`. | both |
Expand All @@ -122,6 +125,36 @@ The ref is the native pager view with these methods attached, so native view met

The package exports these TypeScript types: `PagerViewRef` (the ref type), `PagerViewMethods`, `PagerViewProps`, `PagerViewOnPageScrollEvent`, `PagerViewOnPageSelectedEvent`, `PageScrollStateChangedNativeEvent`, and their `*EventData` payloads.

## Lazy pages

By default, every page renders on mount. With `lazy`, a page renders only once the pager is within `lazyPreloadDistance` pages of it. After that it stays mounted, so it keeps its scroll position and state.

Pages are tracked by `key`, so adding or removing pages doesn't remount the others.

```tsx
<PagerView
style={{ flex: 1 }}
lazy
renderLazyPlaceholder={() => (
<View style={{ flex: 1, justifyContent: 'center' }}>
<ActivityIndicator />
</View>
)}
>
{items.map((item) => (
<View key={item.id}>
<Item item={item} />
</View>
))}
</PagerView>
```

`renderLazyPlaceholder` shows until a page mounts. With the default distance you'll rarely see it: only when an animated `setPage` passes pages that aren't mounted yet, at the start of a swipe with `lazyPreloadDistance={0}`, or when the JS thread is busy. It isn't a loading state for the page's data.

`offscreenPageLimit` is unrelated. It only limits how many pages the Android pager composes, and React still renders every page.

See the [full example](example/src/LazyPagerViewExample.tsx).

## `usePagerView` hook

`usePagerView` bundles the state and handlers you usually wire up yourself: the ref, the active page, the scroll state, the scroll progress, and helpers to add or remove pages and to toggle scrolling, animation and overdrag. It also returns `AnimatedPagerView`, which is `PagerView` wrapped with `Animated.createAnimatedComponent`.
Expand Down
79 changes: 79 additions & 0 deletions e2e/tests/lazy-pager-view.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { freshAppBeforeEach, expect, test } from './support/test.ts';
import type { App, Screen } from './support/test.ts';
import { openExample } from './support/app.ts';

freshAppBeforeEach();

async function openLazyExample(app: App, screen: Screen): Promise<void> {
await openExample(app, screen, 'Lazy PagerView Example');
await expect(screen.getByText('Mounted page 1')).toBeVisible();
await expectMounted(screen, 2);
}

async function expectMounted(screen: Screen, count: number): Promise<void> {
await expect(screen.getByTestId('lazy-mounted-count')).toHaveText(
String(count)
);
}

test('setPage mounts the target and its neighbours only', async ({
app,
screen,
}) => {
await openLazyExample(app, screen);

await screen.getByTestId('lazy-last-page-button').tap();
await expect(screen.getByText('Mounted page 20')).toBeVisible();
await expectMounted(screen, 4);

await screen.getByTestId('lazy-prev-page-button').tap();
await expect(screen.getByText('Mounted page 19')).toBeVisible();
await expectMounted(screen, 5);
});

test('swiping mounts the next neighbour', async ({ app, screen }) => {
await openLazyExample(app, screen);

await screen
.getByText('Mounted page 1')
.swipe({ direction: 'right', momentum: 'fast' });
await expect(screen.getByText('Mounted page 2')).toBeVisible();
await expectMounted(screen, 3);

await screen
.getByText('Mounted page 2')
.swipe({ direction: 'left', momentum: 'fast' });
await expect(screen.getByText('Mounted page 1')).toBeVisible();
await expectMounted(screen, 3);
});

test('preload distance 0 mounts only the pages visited', async ({
app,
screen,
}) => {
await openLazyExample(app, screen);
await screen.getByTestId('lazy-preload-0').tap();

await screen.getByTestId('lazy-next-page-button').tap();
await expect(screen.getByText('Mounted page 2')).toBeVisible();
await expectMounted(screen, 2);

await screen.getByTestId('lazy-next-page-button').tap();
await expect(screen.getByText('Mounted page 3')).toBeVisible();
await expectMounted(screen, 3);
});

test('a visited page keeps its scroll position', async ({ app, screen }) => {
await openLazyExample(app, screen);
await screen
.getByTestId('lazy-page-scroll-0')
.scrollUntilVisible(screen.getByText('Page 1 row 30'), {
direction: 'down',
});

await screen.getByTestId('lazy-next-page-button').tap();
await expect(screen.getByText('Mounted page 2')).toBeVisible();
await screen.getByTestId('lazy-prev-page-button').tap();

await expect(screen.getByText('Page 1 row 30')).toBeVisible();
});
2 changes: 2 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
} from '@react-navigation/native-stack';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { PagerHookExample } from './PagerHookExample';
import { LazyPagerViewExample } from './LazyPagerViewExample';
import { NativeDriverEventsExample } from './NativeDriverEventsExample';
import { NestedHorizontalScrollViewExample } from './NestedHorizontalScrollViewExample';
import {
Expand Down Expand Up @@ -132,6 +133,7 @@ const sections: { title: string; data: Example[] }[] = [
component: ScrollViewInsideExample,
name: 'ScrollView inside PagerView Example',
},
{ component: LazyPagerViewExample, name: 'Lazy PagerView Example' },
],
},
{
Expand Down
Loading
Loading