Skip to content

The Storyteller List Views#

We have 4 flavors of final List views that you can use, that have the following class hierarchy (root class is at the top):

 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)

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 {
  CellType,
  UIStyle,
  Theme,
  DataLoadCompletedEvent
} from '@getstoryteller/react-native-storyteller-sdk';

// Component props
{
  configuration: {
    categories?: Array<string>;
    displayLimit?: number;
    cellType?: CellType;
    theme?: Theme;
    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: { id: string }) => void;
  style: 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.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

You can find more information about StorytellerListViewDelegate on iOS and Android

Adding StorytellerStoriesRowView in the layout#

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

const MyStoriesRow = () => {
  // Ensure StorytellerSDK is initialized
  const rowRef = useRef<StorytellerStoriesRowViewInterface>(null);

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

  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 = () => {
  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, { useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerStoriesGridView,
  CellType,
  UIStyle,
  type StorytellerStoriesGridViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyStoriesGrid = () => {
  // Ensure StorytellerSDK is initialized
  const gridRef = useRef<StorytellerStoriesGridViewInterface>(null);

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

  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: {
    minHeight: 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 {
  CellType,
  UIStyle,
  Theme,
  DataLoadCompletedEvent
} from '@getstoryteller/react-native-storyteller-sdk';

// Component props
{
  configuration: {
    collection?: string;
    displayLimit?: number;
    cellType?: CellType;
    theme?: Theme;
    uiStyle?: UIStyle;
    // Row-only
    visibleTiles?: number;
    // Grid-only (default false)
    isScrollable?: boolean;
  };
  onDataLoadStarted?: () => void;
  onDataLoadCompleted?: (event: DataLoadCompletedEvent) => void;
  onPlayerDismissed?: () => void;
  onTileTapped?: (event: { id: string }) => void;
  style: 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.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 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

You can find more information about StorytellerListViewDelegate on iOS and Android

Adding StorytellerClipsRowView in the layout#

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

const MyClipsRow = () => {
  // Ensure StorytellerSDK is initialized
  const rowRef = useRef<StorytellerClipsRowViewInterface>(null);

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

  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 = () => {
  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, { useRef } from 'react';
import { StyleSheet } from 'react-native';
import {
  StorytellerClipsGridView,
  CellType,
  UIStyle,
  type StorytellerClipsGridViewInterface,
  type DataLoadCompletedEvent,
} from '@getstoryteller/react-native-storyteller-sdk';

const MyClipsGrid = () => {
  // Ensure StorytellerSDK is initialized
  const gridRef = useRef<StorytellerClipsGridViewInterface>(null);

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

  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: {
    minHeight: 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 = () => {
  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: When the component is mounted, you should call reloadData() to load the data into your Storyteller component.
  • 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: Each call triggers a new network request to fetch data
  • 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. Lazy Loading: Use displayLimit to avoid loading too 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 repository, with the most important files being VerticalVideoLists, VerticalVideoListRenderer and StorytellerStoryUnit.

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:

import { FlashList } from '@shopify/flash-list';

const YourComponent = () => {
  return (
    <FlashList
      data={yourData}
      renderItem={({ item }) => (
        <StorytellerStoriesRowView
          configuration={item.configuration}
          ...
        />
      )}
      estimatedItemSize={10}
    />
  );
};