Skip to content

The Storyteller List Views#

React Native exports four list components. The diagram below groups them by layout; the intermediate native classes are not React Native exports:

 StorytellerListView
 ├── StorytellerRowView
 │   ├── StorytellerStoriesRowView
 │   └── StorytellerClipsRowView
 └── StorytellerGridView
     ├── StorytellerStoriesGridView
     └── StorytellerClipsGridView

Choose the one you need depending on two criteria: content and UI behavior. When it comes to content, you can choose between a Stories or a Clips version of the list. As far as UI is concerned, you have 2 options:

Rows are horizontal scrolling lists.

Grids are vertical lists, organized into columns (number of columns can be set on the Theme)

Await SDK initialization before mounting these components. Give rows a height and scrollable grids a bounded height. Call reloadData() after mounting to request content on both platforms; Android also starts an initial load automatically. Handle onDataLoadCompleted to distinguish success, empty content and failure. The getting-started example shows this sequence.

Configuring a Storyteller Stories List Component#

The Storyteller SDK provides two components to display stories: StorytellerStoriesRowView and StorytellerStoriesGridView. Most props are shared. Differences:

  • visibleTiles is supported only on Row views
  • isScrollable is supported only on Grid views

Props#

The shared props between StorytellerStoriesRowView and StorytellerStoriesGridView are the following:

import type { StyleProp, ViewStyle } from 'react-native';
import type {
  CellType,
  UIStyle,
  Theme,
  DataLoadCompletedEvent,
  StorytellerAnalyticsContext,
  TileTappedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

// Shared fields; row/grid-only fields are noted below.
type ListProps = {
  configuration: {
    categories?: Array<string>;
    displayLimit?: number;
    cellType?: CellType;
    theme?: Theme;
    context?: StorytellerAnalyticsContext;
    uiStyle?: UIStyle;
    // Row-only: fine-tune horizontal layout by fixing tiles visible at once
    visibleTiles?: number;
    // Grid-only: control internal scroll handling of the grid (default false)
    isScrollable?: boolean;
  };
  onDataLoadStarted?: () => void;
  onDataLoadCompleted?: (event: DataLoadCompletedEvent) => void;
  onPlayerDismissed?: () => void;
  onTileTapped?: (event: TileTappedEvent) => void;
  style?: StyleProp<ViewStyle>;
};

Configuration Properties#

  • configuration.categories: assigns a list of Story categories to be displayed inside a row or grid
  • configuration.displayLimit: limit number of cells to be displayed in a row or grid
  • configuration.cellType: the style of a cell, use CellType.round or CellType.square. Default is CellType.square
  • configuration.theme: use this property to customize the appearance of the Storyteller List and its various UI elements. You can read more about the various properties in Themes. There is also an example of this in the Showcase App.
  • configuration.context: optional string key/value analytics context attached to this view
  • configuration.uiStyle: sets if Storyteller is rendered in light or dark mode, use UIStyle.auto, UIStyle.light, or UIStyle.dark. Default is UIStyle.auto.
  • configuration.visibleTiles (Row only): number of tiles visible on screen at once (optional, for fine-tuning layout)
  • configuration.isScrollable (Grid only): whether the grid should handle its own scrolling. Default is false. Set to true for standalone scrollable grids, or false when embedding in a parent ScrollView.

Callbacks#

Callbacks used for StorytellerListViewDelegate methods are the following props:

  • onDataLoadStarted: called when the SDK begins loading Story data - this could be used as a trigger to show a loading spinner in your app, for example
  • onDataLoadCompleted: called when the SDK finishes loading Story data. The callback receives a DataLoadCompletedEvent object with:
  • success (boolean): whether the data load was successful
  • error (string): error message if the load failed (empty string if successful)
  • dataCount (number): number of Stories loaded
  • onPlayerDismissed: called when a user exits the Story player view
  • onTileTapped: called when a user taps on a story tile. The callback receives an object with:
  • id (string): the ID of the tapped story
  • type, storyId, clipId, collectionId, and categories: content and category context
  • title (string or null, optional): native content title; an empty string is a valid title
  • tileIndex (number or null, optional): one-based position in the displayed list
  • metadata (string dictionary or null, optional): custom content metadata; missing/null means the source omitted metadata, while {} means explicitly empty

The detailed fields are available with native iOS 11.7.0 and Android 12.0.0. The callback fires once per tile tap before the player opens, including when automatic player opening is disabled. Existing identifiers and categories retain their meaning.

You can find more information about StorytellerListViewDelegate on iOS and Android

Adding StorytellerStoriesRowView in the layout#

import React, { useEffect, useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerStoriesRowView,
  CellType,
  UIStyle,
  type StorytellerStoriesRowViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyStoriesRow = () => {
  // Mount this component only after SDK initialization succeeds.
  const rowRef = useRef<StorytellerStoriesRowViewInterface>(null);

  useEffect(() => {
    rowRef.current?.reloadData();
  }, []);

  const handleDataLoadStarted = () => {
    console.log('Stories data load started');
  };

  const handleDataLoadCompleted = (event: DataLoadCompletedEvent) => {
    console.log('Stories data load completed:', event);
    if (event.success) {
      console.log(`Loaded ${event.dataCount} stories`);
    } else {
      console.error('Error loading data:', event.error);
    }
  };

  const handlePlayerDismissed = () => {
    console.log('Stories player dismissed');
  };

  const handleTileTapped = (event: { id: string }) => {
    console.log('Story tile tapped:', event.id);
  };

  return (
    <StorytellerStoriesRowView
      ref={rowRef}
      configuration={{
        categories: ['category1', 'category2'],
        displayLimit: 10,
        cellType: CellType.round,
        uiStyle: UIStyle.auto,
      }}
      style={styles.container}
      onDataLoadStarted={handleDataLoadStarted}
      onDataLoadCompleted={handleDataLoadCompleted}
      onPlayerDismissed={handlePlayerDismissed}
      onTileTapped={handleTileTapped}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    height: 150,
  },
});

export default MyStoriesRow;
private reference: StorytellerStoriesRowView | null = null;

_onDataLoadStarted = () => {
  console.log(`DataLoadStarted`);
};

_onDataLoadCompleted = (
  success: Boolean,
  error: Error,
  dataCount: number
) => {
  console.log(
    `DataLoadCompleted\n` +
      `success: ${success}, error: ${error}, dataCount: ${dataCount}`
  );
};

reloadData = () => {
  this.reference?.reloadData();
};

render() {
  return (
    <StorytellerStoriesRowView
      ref={(ref: any) => {
        if (ref) this.reference = ref;
      }}
      configuration={{
        categories: this.state.categories,
        cellType: this.state.cellType,
        theme: this.state.theme
      }}
      style={styles.container}
      onDataLoadStarted={this._onDataLoadStarted}
      onDataLoadCompleted={this._onDataLoadCompleted}
    />
  );
}

Adding StorytellerStoriesGridView in the layout#

import React, { useEffect, useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerStoriesGridView,
  CellType,
  UIStyle,
  type StorytellerStoriesGridViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyStoriesGrid = () => {
  // Mount this component only after SDK initialization succeeds.
  const gridRef = useRef<StorytellerStoriesGridViewInterface>(null);

  useEffect(() => {
    gridRef.current?.reloadData();
  }, []);

  const handleDataLoadStarted = () => {
    console.log('Stories grid data load started');
  };

  const handleDataLoadCompleted = (event: DataLoadCompletedEvent) => {
    console.log('Stories grid data load completed:', event);
    if (event.success) {
      console.log(`Loaded ${event.dataCount} stories`);
    } else {
      console.error('Error loading data:', event.error);
    }
  };

  const handlePlayerDismissed = () => {
    console.log('Stories grid player dismissed');
  };

  const handleTileTapped = (event: { id: string }) => {
    console.log('Story tile tapped:', event.id);
  };

  return (
    <StorytellerStoriesGridView
      ref={gridRef}
      configuration={{
        categories: ['category1', 'category2'],
        displayLimit: 8,
        cellType: CellType.square,
        uiStyle: UIStyle.auto,
        isScrollable: true, // optional, default is false
      }}
      style={styles.container}
      onDataLoadStarted={handleDataLoadStarted}
      onDataLoadCompleted={handleDataLoadCompleted}
      onPlayerDismissed={handlePlayerDismissed}
      onTileTapped={handleTileTapped}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    height: 400,
  },
});

export default MyStoriesGrid;
reference: StorytellerStoriesGridView | null = null;

_onDataLoadStarted = () => {
  console.log(`DataLoadStarted`);
};

_onDataLoadCompleted = (
  success: Boolean,
  error: Error,
  dataCount: number
) => {
  console.log(
    `DataLoadCompleted\n` +
      `success: ${success}, error: ${error}, dataCount: ${dataCount}`
  );
};

reloadData = () => {
  this.reference?.reloadData();
};

render() {
  return (
    <StorytellerStoriesGridView
      ref={(ref: any) => {
        if (ref) this.reference = ref;
      }}
      configuration={{
        categories: this.state.categories,
        displayLimit: this.state.displayLimit,
        cellType: this.state.cellType,
        theme: this.state.theme
      }}
      style={styles.container}
      onDataLoadStarted={this._onDataLoadStarted}
      onDataLoadCompleted={this._onDataLoadCompleted}
    />
  );
}

Configuring a Storyteller Clips List Component#

The Storyteller SDK provides two components to display clips: StorytellerClipsRowView and StorytellerClipsGridView. Most props are shared. Differences:

  • visibleTiles is supported only on Row views
  • isScrollable is supported only on Grid views

Props#

The shared props between StorytellerClipsRowView and StorytellerClipsGridView are defined by React component props. Here's the structure:

import type { StyleProp, ViewStyle } from 'react-native';
import type {
  CellType,
  UIStyle,
  Theme,
  StorytellerClipsAdConfiguration,
  DataLoadCompletedEvent,
  StorytellerAnalyticsContext,
  TileTappedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

// Shared fields; row/grid-only fields are noted below.
type ListProps = {
  configuration: {
    collection?: string;
    displayLimit?: number;
    cellType?: CellType;
    theme?: Theme;
    context?: StorytellerAnalyticsContext;
    uiStyle?: UIStyle;
    adConfiguration?: StorytellerClipsAdConfiguration;
    // Row-only
    visibleTiles?: number;
    // Grid-only (default false)
    isScrollable?: boolean;
  };
  onDataLoadStarted?: () => void;
  onDataLoadCompleted?: (event: DataLoadCompletedEvent) => void;
  onPlayerDismissed?: () => void;
  onTileTapped?: (event: TileTappedEvent) => void;
  style?: StyleProp<ViewStyle>;
};

Configuration Properties#

  • configuration.collection: assigns a collection to be displayed inside a row or grid
  • configuration.displayLimit: limit number of cells to be displayed in a row or grid
  • configuration.cellType: the style of a cell, use CellType.round or CellType.square. Default is CellType.square
  • configuration.theme: use this property to customize the appearance of the Storyteller List and its various UI elements. You can read more about the various properties in Themes. There is also an example of this in the Showcase App.
  • configuration.context: optional string key/value analytics context attached to this view
  • configuration.uiStyle: sets if Storyteller is rendered in light or dark mode, use UIStyle.auto, UIStyle.light, or UIStyle.dark. Default is UIStyle.auto.
  • configuration.adConfiguration: Android-only Clips player ad controls; iOS Clips Row and Grid ignore this entire property. On Android, betweenClipsAdProviderOrder accepts StorytellerAdProvider.vast, .gam, and .admob; omit it or pass null for native order, use [] to disable standard between-Clips ads, or pass a populated array to set the allowlist and fallback order. IMA opening pre-roll is configured separately.
  • configuration.visibleTiles (Row only): number of tiles visible on screen at once (optional, for fine-tuning layout)
  • configuration.isScrollable: (Grid view only) whether the grid should handle its own scrolling. Default is false. Set to true for standalone scrollable grids, or false when embedding in a parent ScrollView.

Performance Note: Non-scrollable grids are suitable for when you wish to include the grid in a view hierarchy that already supports scrolling. Using non-scrollable grids with a large number of items and no display limit can significantly degrade performance, as all items are rendered in one go.

Callbacks#

Callbacks used for StorytellerListViewDelegate methods are the following props:

  • onDataLoadStarted: called when the SDK begins loading Clip data - this could be used as a trigger to show a loading spinner in your app, for example
  • onDataLoadCompleted: called when the SDK finishes loading Clip data. The callback receives a DataLoadCompletedEvent object with:
  • success (boolean): whether the data load was successful
  • error (string): error message if the load failed (empty string if successful)
  • dataCount (number): number of Clips loaded
  • onPlayerDismissed: called when a user exits the Clip player view
  • onTileTapped: called when a user taps on a clip tile. The callback receives an object with:
  • id (string): the ID of the tapped clip
  • type, storyId, clipId, collectionId, and categories: content and category context
  • title (string or null, optional): native content title; an empty string is a valid title
  • tileIndex (number or null, optional): one-based position in the displayed list
  • metadata (string dictionary or null, optional): custom content metadata; missing/null means the source omitted metadata, while {} means explicitly empty

The detailed fields are available with native iOS 11.7.0 and Android 12.0.0. The callback fires once per tile tap before the player opens, including when automatic player opening is disabled. Existing identifiers and categories retain their meaning.

You can find more information about StorytellerListViewDelegate on iOS and Android

Adding StorytellerClipsRowView in the layout#

import React, { useEffect, useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerClipsRowView,
  CellType,
  UIStyle,
  type StorytellerClipsRowViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyClipsRow = () => {
  // Mount this component only after SDK initialization succeeds.
  const rowRef = useRef<StorytellerClipsRowViewInterface>(null);

  useEffect(() => {
    rowRef.current?.reloadData();
  }, []);

  const handleDataLoadStarted = () => {
    console.log('Clips data load started');
  };

  const handleDataLoadCompleted = (event: DataLoadCompletedEvent) => {
    console.log('Clips data load completed:', event);
    if (event.success) {
      console.log(`Loaded ${event.dataCount} clips`);
    } else {
      console.error('Error loading data:', event.error);
    }
  };

  const handlePlayerDismissed = () => {
    console.log('Clips player dismissed');
  };

  const handleTileTapped = (event: { id: string }) => {
    console.log('Clip tile tapped:', event.id);
  };

  return (
    <StorytellerClipsRowView
      ref={rowRef}
      configuration={{
        collection: 'my-collection-id',
        displayLimit: 10,
        cellType: CellType.square,
        uiStyle: UIStyle.auto,
      }}
      style={styles.container}
      onDataLoadStarted={handleDataLoadStarted}
      onDataLoadCompleted={handleDataLoadCompleted}
      onPlayerDismissed={handlePlayerDismissed}
      onTileTapped={handleTileTapped}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    height: 200,
  },
});

export default MyClipsRow;
private reference: StorytellerClipsRowView | null = null;

_onDataLoadStarted = () => {
  console.log(`DataLoadStarted`);
};

_onDataLoadCompleted = (
  success: Boolean,
  error: Error,
  dataCount: number
) => {
  console.log(
    `DataLoadCompleted\n` +
      `success: ${success}, error: ${error}, dataCount: ${dataCount}`
  );
};

reloadData = () => {
  this.reference?.reloadData();
};

render() {
  return (
    <StorytellerClipsRowView
      ref={(ref: any) => {
        if (ref) this.reference = ref;
      }}
      configuration={{
        collection: this.state.collection,
        cellType: this.state.cellType,
        theme: this.state.theme
      }}
      style={styles.container}
      onDataLoadStarted={this._onDataLoadStarted}
      onDataLoadCompleted={this._onDataLoadCompleted}
    />
  );
}

Adding StorytellerClipsGridView in the layout#

import React, { useEffect, useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerClipsGridView,
  CellType,
  UIStyle,
  type StorytellerClipsGridViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyClipsGrid = () => {
  // Mount this component only after SDK initialization succeeds.
  const gridRef = useRef<StorytellerClipsGridViewInterface>(null);

  useEffect(() => {
    gridRef.current?.reloadData();
  }, []);

  const handleDataLoadStarted = () => {
    console.log('Clips grid data load started');
  };

  const handleDataLoadCompleted = (event: DataLoadCompletedEvent) => {
    console.log('Clips grid data load completed:', event);
    if (event.success) {
      console.log(`Loaded ${event.dataCount} clips`);
    } else {
      console.error('Error loading data:', event.error);
    }
  };

  const handlePlayerDismissed = () => {
    console.log('Clips grid player dismissed');
  };

  const handleTileTapped = (event: { id: string }) => {
    console.log('Clip tile tapped:', event.id);
  };

  return (
    <StorytellerClipsGridView
      ref={gridRef}
      configuration={{
        collection: 'my-collection-id',
        displayLimit: 10,
        cellType: CellType.square,
        uiStyle: UIStyle.auto,
        isScrollable: true, // optional, default is false
      }}
      style={styles.container}
      onDataLoadStarted={handleDataLoadStarted}
      onDataLoadCompleted={handleDataLoadCompleted}
      onPlayerDismissed={handlePlayerDismissed}
      onTileTapped={handleTileTapped}
    />
  );
};

const styles = StyleSheet.create({
  container: {
    height: 400,
  },
});

export default MyClipsGrid;
reference: StorytellerClipsGridView | null = null;

_onDataLoadStarted = () => {
  console.log(`DataLoadStarted`);
};

_onDataLoadCompleted = (
  success: Boolean,
  error: Error,
  dataCount: number
) => {
  console.log(
    `DataLoadCompleted\n` +
      `success: ${success}, error: ${error}, dataCount: ${dataCount}`
  );
};

reloadData = () => {
  this.reference?.reloadData();
};

render() {
  return (
    <StorytellerClipsGridView
      ref={(ref: any) => {
        if (ref) this.reference = ref;
      }}
      configuration={{
        collection: this.state.collection,
        displayLimit: this.state.displayLimit,
        cellType: this.state.cellType,
        theme: this.state.theme
      }}
      style={styles.container}
      onDataLoadStarted={this._onDataLoadStarted}
      onDataLoadCompleted={this._onDataLoadCompleted}
    />
  );
}

Imperative API: reloadData()#

All Storyteller list components expose a reloadData() method through refs, which allows you to manually refresh the component's data.

When to Use reloadData()#

  • On mount: After initialization, call reloadData() on the mounted component to request fresh content on both platforms. Android also initiates an automatic load; iOS configuration can display cached content before a refresh.
  • User-triggered refresh: When the user pulls to refresh or taps a refresh button
  • Data invalidation: When you know the underlying data has changed (e.g., after following a category)
  • Configuration changes: After changing categories, collections, or other configuration properties
  • Network recovery: After recovering from a network error

Performance Considerations#

  • Avoid excessive calls: Don't call reloadData() too frequently (e.g., multiple times per second)
  • Network impact: Reloading can fetch data or refresh cached content; avoid treating it as a guaranteed new network request
  • User experience: Consider showing loading indicators during refresh operations

Performance Best Practices#

Display Limits#

The displayLimit configuration property helps control memory usage and rendering performance:

<StorytellerStoriesGridView
  configuration={{
    categories: ['featured'],
    displayLimit: 20, // Limit to 20 items
  }}
  style={{ minHeight: 400 }}
/>

Guidelines:

  • Rows: Limit to 10-20 items for optimal scrolling performance
  • Grids: Limit to 20-50 items depending on device capabilities
  • Non-scrollable grids: Always use displayLimit to prevent rendering all items at once

Scrollable vs Non-Scrollable#

Choose the appropriate scrolling behavior based on your layout:

Scrollable (isScrollable: true):

  • Use when the list is the primary scrolling content
  • Better performance for long lists
  • Handles its own scroll events

Non-Scrollable (isScrollable: false - default):

  • Use when embedding within a parent ScrollView or FlashList
  • Renders all items within displayLimit at once
  • No internal scroll handling
// Embedded in parent ScrollView
<ScrollView>
  <Text>Header Content</Text>
  <StorytellerStoriesRowView configuration={{ categories: ['featured'], displayLimit: 10 }} style={{ height: 200 }} />
  <Text>More Content</Text>
</ScrollView>

Memory Management Tips#

  1. Display Size: Use displayLimit to limit rendered tiles; it does not guarantee that network responses contain only that many items
  2. Category Filtering: Only load categories that users are interested in
  3. Conditional Rendering: Don't render Storyteller views that are far off-screen
  4. Theme Optimization: Avoid complex custom themes when possible

More Advanced Layout#

More advanced examples of layouts where multiple Storyteller Rows and Grids are used can be viewed in our React Native Showcase:

Notes for Android Implementations#

When implementing Storyteller views within scrollable lists on Android, there are critical performance and rendering considerations:

⚠️ Important: FlatList Compatibility Issues#

FlatList must not be used with Storyteller views on Android. This is because:

  • FlatList's virtualization conflicts with Storyteller's native view rendering
  • It can cause crashes and other rendering issues
  1. For Simple Lists (Recommended for < 10 items)
  2. Use ScrollView for straightforward implementations
  3. Best for fixed, smaller lists of content

  4. For Longer Lists (Recommended for 10+ items)

  5. Use FlashList
  6. Provides better performance and memory management
  7. Fully compatible with Storyteller views

Example using FlashList 2.x (after SDK initialization). Each row requests data after mounting and after its category changes:

import React, { useEffect, useRef } from 'react';
import { View } from 'react-native';
import { FlashList } from '@shopify/flash-list';
import {
  StorytellerStoriesRowView,
  type StorytellerStoriesRowViewInterface,
} from '@getstoryteller/react-native-storyteller-sdk';

type Row = { id: string; categoryId: string };

function StoriesRow({ categoryId }: { categoryId: string }) {
  const ref = useRef<StorytellerStoriesRowViewInterface>(null);
  useEffect(() => {
    ref.current?.reloadData();
  }, [categoryId]);
  return (
    <StorytellerStoriesRowView
      ref={ref}
      configuration={{ categories: [categoryId], displayLimit: 10 }}
      style={{ height: 200 }}
      onDataLoadCompleted={({ success, error }) => {
        if (!success) console.warn('Row load failed:', error);
      }}
    />
  );
}

export function StoriesFeed({ rows }: { rows: Row[] }) {
  return (
    <View style={{ flex: 1 }}>
      <FlashList
        data={rows}
        keyExtractor={item => item.id}
        renderItem={({ item }) => <StoriesRow categoryId={item.categoryId} />}
      />
    </View>
  );
}