The Vega Scrolling Sample App demonstrates how to build a home page grid layout using Vega components.
The Vega Scrolling Sample App is optimized using the best practices for each component such as memoizing and avoiding state updates. The app is also configurable, allowing you to easily modify the elements while maintaining the optimizations. You can choose between two list components from Vega: FlashList and Carousel. These components were selected because they are the most performant lists. To learn more about list components, see the List components section and Demo videos.
Before you launch the sample app, make sure that you have installed the Vega SDK.
After you download the source code from GitHub, you can build the Vega Scrolling Sample App from the command line to generate VPKG files. The VPKG files run on the Vega Virtual Device and Vega Fire TV Stick.
You can also use Vega Studio with Visual Studio Code to build the app.
- 
At the command prompt, navigate to the Vega Scrolling Sample App source code directory. 
- 
To install the app dependencies, run the following command. npm install
- 
To build the app to generate .vpkg files, run the following command. npm run build:app
- 
At the command prompt, in the build folder, verify that you generated the VPKG files for your device's architecture. - armv7-release/keplerscrollingapp_armv7.vpkg—generated on x86_64 and Mac-M series devices to run on the Vega Fire TV Stick.
- x86_64-release/keplerscrollingapp_x86_64.vpkg—generated on x86_64 device to run on the KVD.
- aarch64-release/keplerscrollingapp_aarch64.vpkg—generated on Mac M-series device to run on the KVD.
 
- 
To start the Vega Virtual Device, at the command prompt, run the following command. kepler virtual-device start
- 
Go to the directory where you placed the VPKG files. 
- 
To install and launch the app on the Vega Virtual Device, run the following command, depending on your device architecture. - 
On Mac M-series based devices. kepler run-kepler build/aarch64-release/keplerscrollingapp_aarch64.vpkg
- 
On x86_64 based devices. kepler run-kepler build/x86_64-release/keplerscrollingapp_x86_64.vpkg
 
- 
- 
Turn on your Fire TV Stick. 
- 
To install and launch the app on your Fire TV Stick, run the following command. kepler run-kepler build/armv7-release/keplerscrollingapp_armv7.vpkg
CarouselDemo.mp4
FlashlistDemo.mp4
The config.ts file allows you to customize the visual appearance and behavior of the Scrolling App. These configuration values are also used in the optimization features of the FlashList and Carousel components. When you make configuration changes, the lists are optimized for the new layout. It is recommended that you first make changes in the config.ts file before making changes in other files.
Card configuration controls the appearance of three different card types: HERO, VERTICAL, and REGULAR. For more information about the card types, see Card variations.
For each card type, you can configure the following:
- HEIGHT: Vertical space for the title.
- FONT_SIZE: Text size of the title.
- FONT_WEIGHT: Boldness of the title (for example,- 300and- 700).
- HEIGHT: Card height.
- WIDTH: Card width.
- BORDER_RADIUS: Corner rounding of the card.
- RESOLUTION: Image resolution (- 720,- 400,- 260).
- GAP: Horizontal spacing between cards.
- ROW_GAP: Vertical spacing between rows.
Controls the layout of card rows. For each card type, you can set the following:
- ITEM_COUNT: Number of items to display.
- HEIGHT: Total row height (automatically calculated).
- WIDTH: Total width of row item (automatically calculated).
This app showcases two types of lists:FlashList and Carousel. Lists can be switched by setting the currentList variable. FlashList is the "default" list for the Scrolling app and used out-of-the-box. And most references in the documentation refer to the FlashList implementation. Some typescript errors might occur when using Carousel, but those errors can be ignored.
export const currentList: ListType = Carousel // By default set to 'FlashList';For more information, see List components.
The Scrolling app implements a grid layout populated with rounded Card components, designed to simulate a typical movie or TV content browsing interface. This layout follows a common design pattern known as a horizontal stack layout, widely used in TV and streaming applications.
The grid is implemented using nested list components. Each row has Card components stored in a horizontally scrollable list (CardRow). Then these horizontal lists are arranged in a vertical list to enable vertical scrolling (Grid). The FlashlistGrid.tsx file includes the implementation of the vertical list.
- 
Horizontal list ( CardRow)- Each row contains a horizontal list of cards.
- Represents a single category or content type.
- Allows horizontal scrolling within each row.
 
- 
Vertical list ( Grid)- Combines multiple horizontal rows vertically.
- Enables overall grid navigation and scrolling.
 
The rows of the grid have different sized cards. This creates a non-uniform, or heterogenous list structure for the vertical list. The FlashList and Carousel components rely on predictable and uniform item sizes to optimize performance. Uniform item sizes allows these lists to pre-calculate the position and layout of items. However, with heterogeneous items, each row potentially presents a different height and layout. This forces the rendering engine to perform complex, computationally expensive calculations for every scroll and item placement.
To mitigate the challenge, you can instruct the lists about the different item and their dimensions. If the vertical list knows all the possible types of rows and their dimensions, then it is able to perform those pre-calculations. Also, the lists create separate recycling pools for each item type to avoid additional layout calculations. This solution can be implemented by using the getItemType and overrideItemLayout props for FlashList and the itemDimensions and getItemForIndex for Carousel. These props allow us to explicitly define the different types of rows and the dimensions for each one. For Carousel, each row must have a different component. The Scrolling app includes the base implementation of the CardRow, and then created variations of that component for the different card types CardRowVariations.
Note: Even with this solution, lists with uniform items perform better than heterogenous lists. Among other reasons, maintaining different recycling pools of the different item types consume more memory.
Card is the fundamental component of this app. It populates the Grid and comprises the bulk of the rendering. The Scrolling app renders >3000 Card components on startup. See the following details about Card.
The Card component is comprised of very few elements. Card is an image with a border focus effect and is easily recycled and rendered.
State updates causes re-renders. Because there are many Card components on-screen, any re-render is costly. State updates do not work well with FlashList and Carousel. See the Flashlist recommendations for state updates.
Image selection is critical for optimizing visual quality and performance. Image resolution should precisely match the container size. Larger resolutions are scaled down to match the container. The extra resolution wastes memory and causes network delay. You must pick the minimum resolution for images to be clear and crisp to optimize your app for the typical TV viewing distance of 10 feet.
TV apps often have different types of cards to provide different emphasis to their content. These cards can vary in both shape and size. The following explains the details of the regular card, vertical card, and hero card.
The Regular Card is the foundational element of the grid, designed as a compact and versatile component that enables broad catalog representation while maximizing information density. Its smaller size makes it ideal for displaying extensive content collections, providing quick, at-a-glance information that remains readable even when numerous items are presented simultaneously.
- height: 175 px
- width: 300 px
- Items per row: 40
The Vertical Card introduces a distinctive elongated profile that breaks visual monotony and offers a unique browsing experience. Its elongated design provides visual contrast and supports alternative content presentation, allowing interfaces to create rhythm and visual interest within the grid. This card type is particularly effective for highlighting vertically-oriented content and accommodating different media types that don't fit traditional horizontal layouts.
- height: 400 px
- width: 250 px
- Items per row: 80
The Hero Card serves as a visual anchor, drawing immediate user attention to key content through its significantly larger dimensions. Positioned strategically, typically at the top of a page or section, this card type creates a strong visual hierarchy that guides users to the most important or featured content. Its substantial size introduces more complex focus interactions, challenging traditional navigation patterns and providing a dynamic focal point in the interface.
- height: 400 px
- width: 700 px
- Items per row: 50
Lists are a vital part of TV apps. Lists are used to display an content catalogue of an app similarly to the Scrolling app. Vega provides several options for creating lists of elements:
- ScrollView
- FlatList
- FlashList
- Carousel
The Scrolling app demonstrates FlashList and Carousel components. These were selected because they are the most performant lists. Their strength comes from recycling list items. By default, the Scrolling app uses FlashList to create its layout. Carousel can be toggled in the configuration (for more information, see Toggle list type). The Scrolling app uses nested lists with a horizontal list for each row (CardRow) and a vertical list (Grid) to create a grid.
Performance test scripts can be found in the test/performance folder. The generalTest.py script tests the entire app layout. This is the test that was used to obtain the performance numbers in the following table for the FlashList and Carousel components. To run and set up performance testing, see Measure Your App KPIs.
| List component | PO | P50 | P90 | P95 | P99 | P100 | stdev | variance | 
|---|---|---|---|---|---|---|---|---|
| FlashList | 99.7 | 99.45 | 97.98 | 97.68 | 97.68 | 97.68 | 0.74 | 0.55 | 
| Carousel | 99.8 | 99.38 | 98.88 | 98.63 | 98.63 | 98.63 | 0.3 | 0.09 | 
Separate Grid and CardRow components were created for each list type and are stored in the ../src/components/FlashList and ../src/components/Carousel directories. Where possible, common code is shared in src/utils/listUtils.tsx. Most references in the documentation refer to the FlashList implementation of the components.
FlashList is a third party library built for React Native and adapted for the Vega platform. FlashList is a cross-platform scrolling component that uses recycling behavior to be more performant.
The performance of FlashList depends on precise data being passed in through its props. This is especially true in a nested list layout like in the Scrolling app. Whenever possible, values should be pre-calculated outside of the components. In the Scrolling app the CARD_CONFIG and ROW_CONFIGis used to store these calculated values (see config.ts in the Configure the app section). This avoids calculations during component renders.
The following includes the props leveraged in this app to optimize FlashList performance. See Shopify's Flashlist documentation for more info.
The estimatedItemSize prop is the size of the items in the list.
The estimatedItemSize prop is the width of the Card and any margin or other spacing. marginRight is used in the Card components to add gaps between them.
The estimatedItemSize prop is more complicated for the vertical list because each row has a different height. (To learn more, see the Heterogenous list section.) Instead of using this prop, vertical list leverages the overrideItemLayout prop to explicitly specify the size of each row. Because estimatedItemSize is a required prop, a placeholder value is used for it. This placeholder value is the average height of the rows. overrideItemLayout overrides the value of estimatedItemSize so the placeholder is never used.
The overrideItemLayout prop is used to explicitly define the size of every row, is necessary for the vertical list, and overrides estimatedItemSize. Pre-calculated heights from ROW_CONFIG are used.
const overrideItemLayout = useCallback(
  (
    layout: {
      span?: number;
      size?: number;
    },
    item: RowData,
  ) => {
    layout.size = ROW_CONFIG[item.cardType].HEIGHT;
  },
  [],
);The getItemType prop helps FlashList identify the different rows in the vertical list. FlashList uses this to create separate pools for recycling. For example, this prevents rows with HERO cards from being recycled using rows of VERTICAL cards.
const getItemType = useCallback((item: RowData) => {
  return item.cardType;
}, []);The drawDistance prop is used in the vertical list to increase the number of items that FlashList renders. Vertical recycling is slower than horizontal recycling. So when scrolling vertically, users are more likely see blank areas or see rows visibly being recycled. Specifying drawDistance helps provide a smoother user experience. However, this comes at a performance cost, so evaluate the value of this in your own app.
The keyExtractor prop is used to create a unique ID for each list item. This is especially important to help the app properly distinguish between items when focusing.
It is recommended to not use the ItemSeparatorComponent prop. Implementing a gap using margin is slightly more performant.
Carousel was built natively for Vega which allows it perform intensive scrolling operations on the native threads. Carousel has the best performance out-of-the-box compared to the other scrolling solutions. However, because it is a native Vega component, it does not work on cross-platform React Native apps. See the Vega Carousel docs for more details.
Unlike FlashList, Carousel requires users to define the height and width of list items.
Precomputed values from CARD_CONFIG are used.
The items of the vertical list are rows. The width of the rows, is the visible, scrollable part of the row. It is not the combined total with of all the cards in the row. If the row fills the entire width, it matches the screen size. For example, a 1920p display would be 1920px. The scrolling app has spacing to the left of the grid. The width is calculated in the following example.
const CAROUSEL_ITEM_DIMENSIONS = [
  {
    view: CARD_ROW_VARIATIONS.HERO,
    dimension: {
      width: SCREEN_DIMENSION.width - PAGE_PADDING,
      height: ROW_CONFIG.HERO.HEIGHT,
    },
  },
  ...
];Note: All rows of a grid layout have the same width so the same value can be used.
getItemForIndex is used to inform the Carousel what each component is used to render each element. Wrapper components were created for each CardType (CardViartions and CardRowVariations) so that Carousel can better distinguish between the items. Each CardType has different dimensions so Carousel needs to know which item  appears where.
These props are used in the horizontal lists to fix the focused item to the center when scrolling. For more information, see the Customizing scrolling section.
Two common types of scrolling behaviors are natural and fixed scrolling. The difference between two types is how the list scrolls relative to the focused element on the screen.
Focus will freely move between all the elements in the current viewable window. When focus moves beyond the viewable window, the window will scroll to reveal the next focusable element.
Focus is fixed, or trapped to one position on the screen. Common positions are at the start, middle, or end of a list. When focus changes, the window scrolls to move the newly focused element to the same position.
To implement fixed scrolling, the default scrolling in FlashList is disabled, and scrollToIndex() is programmatically called to manually scroll the list to the appropriate positions. Because the Scrolling app uses a nested list, horizontal and vertical scrolling must be triggered. You must create functions for scrolling horizontally and vertically, and then pass the functions into the Card component.
- 
Disable scrolling. export const FlashlistGrid = () => { ... return ( <FlashList ref={verticalRef} scrollEnabled={false} // disables the natural scrolling ... /> ); } 
- 
Set up a ref for FlashList.export const FlashlistGrid = () => { const verticalRef = useRef<FlashList<RowData>>(null); ... return ( <FlashList ref={verticalRef} ... /> ); } 
- 
Create the scroll function. You use the ref to the FlashListto invoke thescrollToIndex()function. This function has two important parameters:- index: Determines which card is scrolled to.
- viewPosition: Determines the fixed position of the focused element. The value is between- 0to- 1.0that represents a fraction of the viewable window. The following are some common values:- 0: Focus is fixed to the start of the viewable window.
- 0.5: Focus is fixed to the middle of the viewable window.
- 1.0: Focus is fixed to the end of the viewable window.
 
 export const FlashlistGrid = () => { const verticalRef = useRef<FlashList<RowData>>(null); const scrollVertically = (rowIndex: number) => { if (verticalRef.current) { verticalRef?.current?.scrollToIndex({ animated: true, index: rowIndex, viewPosition: 0.5, }); } }; ... } 
- 
Pass the scroll function to child element. You need to trigger scroll when the focus is on the card. You must pass the scrollVertically()function to theCardcomponent. Next, you create ascrollHorizontally()function inCardRowand pass it toCard.export const FlashlistGrid = () => { const renderRow = ({item}: {item: RowData; index: number}) => { const CardRow = CARD_ROW_VARIATIONS[item.cardType]; // passing scroll function to CardRow which will eventually pass to Card return <CardRow rowData={item} scrollVertically={scrollVertically} />; } ... } 
- 
Trigger scroll when the focus is on the card. Next, in the card you trigger the scroll animation during focus. export const Card = ({cardType, data, scroll}: CardProps) => { const focusHandler = () => { ... // scrolling both horizontally and vertically if (currentList === 'FlashList') { scroll.vertically?.(data.rowIndex); scroll.horizontally?.(data.index); } }; return ( <Pressable ref={isFirstCard ? firstCardRef : undefined} onFocus={focusHandler} ... > </Pressable> ) }; 
Natural Scrolling is the default scrolling behavior in FlashList, FlatList, and Scrollview.
To enable scrolling and comment out the scrolling functions in Card
export const FlashlistGrid = () => {
    ...
    return (
        <FlashList
        scrollEnabled={true}
        ...
        />
    );
}
export const FlashlistCardRow = () => {
    ...
    return (
        <FlashList
        scrollEnabled={true}
        ...
        />
    );
}
export const Card = ({cardType, data, scroll}: CardProps) => {
  const focusHandler = () => {
    //   scroll.vertically?.(data.rowIndex);
    //   scroll.horizontally?.(data.index);
  };
  ...
};This is the default scrolling behavior of Carousel.
To fix the different areas of the scrollable window for horizontal scrolling in CarouselCardRow
const cardWidth = ROW_CONFIG[rowData.cardType].WIDTH;
const numItems = screenWidth / cardWidth;
initialStartIndex = Math.floor(numItems / 2);
firstItemOffset = (screenWidth - cardWidth) / 2;
numOffsetItems = Math.ceil(numItems - 1);
...
<Carousel
    initialStartIndex={initialStartIndex}
    firstItemOffset={firstItemOffset}
    numOffsetItems={numOffsetItems}
    ...
/>Set the focusIndicatorType prop to 'natural'.
In television application design, the focus state is crucial for user navigation. The Scrolling app implements a focus effect through a bright border, or "focus ring", within the Card component. The distinctive visual indicator helps users track their current selection across the grid interface, transforming a static display into an interactive experience by providing clear and animated feedback about the active item.
The optimal method to apply a focus effect is using the React Native Animated API. For more information, see React Native Animations.
The main advantage of implementing the focus effect through the Animated API is leveraging native animations. The Animated API has an option called useNativeDriver. This sends the animation configuration to the native thread and performs the animation natively. This frees up the JS thread for other purposes.
However, there are limits to this approach. Only specific style properties like opacity and transform can be implemented using the useNativeDriver. For more information, see React Native Animations.
Another advantage of this approach is that it requires no state updates and greatly decreases the number of re-renders.
Because borderWidth can't be animated using the native driver, opacity is used. An Animated.View is placed behind the image in Card. The Animated.View is initially invisible (opacity = 0). But, on focus, the animation triggers to make it visible (opacity = 1.0).
const animationValue = useAnimatedValue(0);
const focusHandler = useCallback(() => {
  Animated.timing(animationValue, {
    toValue: 1,
    duration: 100,
    useNativeDriver: true,
  }).start();
}, [data.index, data.rowIndex]);
<Pressable onFocus={focusHandler}>
  ...
  <Animated.View
    style={[
      {
        position: 'absolute',
        zIndex: -1,
        backgroundColor: '#de4788',
        opacity: animationValue,
      },
    ]}
  />
</Pressable>;InteractionManager allows you to schedule animations. By using the InteractionManager.runAfterInteractions() command, you can delay an animations until all other animations and interactions are completed. This reduces the chance of overloading the JS thread and helps to improve overall responsiveness and fluidity of the Scrolling app. However, this might not be appropriate for every app. runAfterInteractions() postpones after all animations, including scroll animations. The focus effect might look like it's "lagging" because it is not triggered until after the scroll animation.
const focusAnimation = useCallback(() => {
  Animated.timing(animationValue, {
    toValue: 1,
    duration: 100,
    useNativeDriver: true,
  }).start();
}, [data.index, data.rowIndex]);
const focusHandler = useCallback(() => {
  InteractionManager.runAfterInteractions(focusAnimation);
}, [data.index, data.rowIndex, focusAnimation]);Typically TV apps start with focus on one of the items. This serves as the starting point for navigation and the user experience. In the scrolling app, the focus is automatically on the first Card and the first CardRow. This is done through the FocusManager API.
FocusManager uses a ref on an element to focus on it. You must get the ref specifically of the first Card. Because Card(../src/components/Card.tsx) is re-used throughout the grid, you use a boolean check to identify the first card, and then the ref can be conditionally applied. FocusManager.focus() is called in a usEffect to ensure that the first child is focused as soon as it is rendered.
const firstCardRef = useRef<View>(null);
const isFirstCard = data.index === 0 && data.rowIndex === 0;
useEffect(() => {
  if (isFirstCard) {
    FocusManager.focus(findNodeHandle(firstCardRef?.current));
  }
}, [isFirstCard]);
...
return (
  <Pressable
    ref={isFirstCard ? firstCardRef : undefined}
    ...
)hasTVPreferredFocus is another method to automatically draw focus onto components. Some edge cases exist where this might not perform well. For example, in a multi-page app, focus is reset between the different pages. hasTVPreferredFocus might not recognize the page change and focus may linger on the previous page. FocusManager provides a more reliable and intentional method to focus items.
The most intuitive method of applying a focus border is to have a focus style and toggle it on and off when focus is triggered.
const [isFocused, setIsFocused] = useState(false);
const focusHandler = () => {
  setIsFocused(true);
};
const focusedStyle = { borderColor: "green"}
<Pressable
    onFocus={focusHandler}
    style={isFocused ? focusedStyle : undefined}
    ...
/>;This is not recommended. This relies on state updates which cause re-renders and strains the JS thread.
API pagination is a performance optimization feature that loads content on-demand as users navigate through the app. Instead of loading all cards at once, the app initially loads a small subset and fetches additional cards when users approach the end of a row. This approach significantly improves app startup time and memory usage, especially important for TV apps with large content catalogs.
API Pagination is enabled by default in featureConfig.ts
The pagination system integrates seamlessly with both FlashList and Carousel components, providing smooth user experiences while maintaining optimal performance. The system uses a threshold-based trigger mechanism that anticipates user needs by fetching new content before users reach the end of currently loaded items.
- Faster app startup: Initial load times are reduced by loading only essential content.
- Memory efficiency: Lower memory footprint by loading content incrementally.
- Network optimization: Reduces initial bandwidth usage and spreads network requests over time.
- Smooth user experience: Predictive loading ensures content is available when users need it.
The pagination system is built on Redux with the following key components.
Each row maintains its own pagination state through the PaginationState interface.
Example:
interface PaginationState {
  isLoading: boolean;        // Current loading status
  hasMore: boolean;          // Whether more content is available
  error: string | null;      // Error message if request fails
  lastRequestedIndex: number; // Prevents duplicate requests
}The system uses several Redux actions to manage pagination state.
- setPaginationLoading: Updates loading status for a specific row.
- setPaginationError: Handles error states and messages.
- setLastRequestedIndex: Tracks the last requested card index to prevent duplicates.
- addRowData: Adds newly fetched cards to the row data.
Async operations are handled through Redux thunks in src/thunks/paginationThunks.ts.
Example:
export const fetchMoreCards = (
  cardData: CardData,
  paginationBatchSize: number,
  rowIndex: number
): AppThunk => async (dispatch, getState, api) => {
  // Pagination logic implementation
};Pagination is controlled through the feature configuration.
Example:
// src/config/featureConfig.ts
export const FEATURES_CONFIG = {
  API_PAGINATION: true, // Set to false to disable pagination
};Each row type has its own pagination settings in ROW_CONFIG.
Example:
// src/config/layout/rowConfig.ts
export const ROW_CONFIG: RowConfig = {
  HERO: {
    ITEM_COUNT: 50,      // Total items available
    API_PAGE_SIZE: 10,   // Items loaded per pagination request
    // ... other config
  },
  VERTICAL: {
    ITEM_COUNT: 80,
    API_PAGE_SIZE: 15,
    // ... other config
  },
  REGULAR: {
    ITEM_COUNT: 40,
    API_PAGE_SIZE: 15,
    // ... other config
  },
};The app initially loads API_PAGE_SIZE number of cards for each row. This provides immediate content while keeping initial load times fast.
Pagination uses a threshold-based approach to determine when to fetch more content.
Example:
const shouldTrigger = cardData.index >= currentRowLength - paginationBatchSize + 1;This means pagination triggers when a user focuses on a card that's within paginationBatchSize cards of the end of currently loaded content. This predictive approach ensures smooth user experiences.
The system prevents duplicate requests through several mechanisms.
Example:
const isAlreadyRequested = cardData.index <= pagination.lastRequestedIndex;
const isCurrentlyLoading = pagination.isLoading;
const hasNoMoreData = !pagination.hasMore;
if (!shouldTrigger || isAlreadyRequested || isCurrentlyLoading || hasNoMoreData) {
  return [];
}Pagination is triggered through the card focus system. When a user focuses on a card, the onFocusUpdate callback checks if pagination should be triggered.
Example:
const onFocusUpdate = useCallback((cardData: CardData) => {
  if (FEATURES_CONFIG.API_PAGINATION) {
    dispatch(
      fetchMoreCards(
        cardData,
        paginationBatchSize,
        rowIndex
      )
    );
  }
}, [dispatch, paginationBatchSize, rowIndex]);The system uses dependency injection to provide API methods to thunks.
Example:
// src/services/dataService.ts
export const api = {
  cardApi: {
    fetchCards: mockCardFetch,
  },
};The current implementation includes a mock API that simulates network delays.
Example:
const mockCardFetch = async (
  rowIndex: number, 
  startIndex: number, 
  count: number
): Promise<CardData[]> => {
  // Simulate network delay
  await delay(1000);
  
  const rowData = ROW_DATA[rowIndex];
  if (!rowData) {
    return [];
  }
  
  // Return a slice of the cards
  return rowData.data.slice(startIndex, startIndex + count);
};To integrate with real APIs, replace the mockCardFetch function with your actual API implementation.
Example:
const realCardFetch = async (
  rowIndex: number, 
  startIndex: number, 
  count: number
): Promise<CardData[]> => {
  const response = await fetch(`/api/cards/${rowIndex}?start=${startIndex}&count=${count}`);
  const data = await response.json();
  return data.cards;
};
export const api = {
  cardApi: {
    fetchCards: realCardFetch,
  },
};The pagination system includes comprehensive error handling.
When API requests fail, the system performs the following:
- Logs the error for debugging.
- Updates the pagination state with error information.
- Stops the loading indicator.
- Allows users to retry by navigating away and back.
Example:
try {
  const newCards = await api.cardApi.fetchCards(/*...*/);
  // Handle success
} catch (error) {
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
  console.error(`[API PAGINATION] Failed to fetch more cards for row ${rowIndex}:`, error);
  dispatch(setPaginationError({ rowIndex, error: errorMessage }));
  return [];
}The system automatically clears error states when successful requests are made, ensuring users don't get stuck in error states.
Pagination helps manage memory by the following:
- Loading only visible and near-visible content.
- Working with FlashList's recycling mechanism.
- Preventing memory bloat from loading entire catalogs.
The system optimizes network usage through the following:
- Predictive loading based on user navigation patterns.
- Request deduplication to prevent unnecessary calls.
- Configurable batch sizes to balance performance and user experience.
Pagination works seamlessly with FlashList's recycling.
- New cards are added to existing data arrays.
- FlashList automatically handles rendering updates.
- Recycling continues to work with dynamically loaded content.
The system includes comprehensive logging for debugging.
Example:
console.log(`[API PAGINATION] Triggering pagination for row ${rowIndex}, card ${cardData.index}`);
console.log(`[API PAGINATION] Number of Cards Retrieved: ${newCardsLen}`);
console.log(`[API PAGINATION] Row ${rowIndex} updated and now has ${row.data.length} cards`);Pagination not triggering:
- Check that FEATURES_CONFIG.API_PAGINATIONistrue.
- Verify that API_PAGE_SIZEis configured correctly.
- Ensure focus events are working properly.
Duplicate requests:
- The system should prevent duplicates automatically.
- Check console logs for request deduplication messages.
- Verify lastRequestedIndexis being updated correctly.
Performance issues:
- Consider adjusting API_PAGE_SIZEvalues.
- Monitor network request frequency.
- Check for memory leaks in long-running sessions.
You can inspect pagination state through Redux DevTools.
Example:
// Current pagination state for row 0
state.rows[0].pagination- Configure appropriate page sizes: Balance between too many small requests and too few large requests.
- Monitor network conditions: Consider implementing adaptive batch sizes based on connection quality.
- Monitor performance: Track metrics to ensure pagination improves rather than degrades user experience.







