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:
visibleTilesis supported only on Row viewsisScrollableis 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 gridconfiguration.displayLimit: limit number of cells to be displayed in a row or gridconfiguration.cellType: the style of a cell, useCellType.roundorCellType.square. Default isCellType.squareconfiguration.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, useUIStyle.auto,UIStyle.light, orUIStyle.dark. Default isUIStyle.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 isfalse. Set totruefor standalone scrollable grids, orfalsewhen 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 exampleonDataLoadCompleted: called when the SDK finishes loading Story data. The callback receives aDataLoadCompletedEventobject with:success(boolean): whether the data load was successfulerror(string): error message if the load failed (empty string if successful)dataCount(number): number of Stories loadedonPlayerDismissed: called when a user exits the Story player viewonTileTapped: 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:
visibleTilesis supported only on Row viewsisScrollableis 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 gridconfiguration.displayLimit: limit number of cells to be displayed in a row or gridconfiguration.cellType: the style of a cell, useCellType.roundorCellType.square. Default isCellType.squareconfiguration.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, useUIStyle.auto,UIStyle.light, orUIStyle.dark. Default isUIStyle.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 isfalse. Set totruefor standalone scrollable grids, orfalsewhen 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 exampleonDataLoadCompleted: called when the SDK finishes loading Clip data. The callback receives aDataLoadCompletedEventobject with:success(boolean): whether the data load was successfulerror(string): error message if the load failed (empty string if successful)dataCount(number): number of Clips loadedonPlayerDismissed: called when a user exits the Clip player viewonTileTapped: 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
displayLimitto 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
displayLimitat 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#
- Lazy Loading: Use
displayLimitto avoid loading too many items - Category Filtering: Only load categories that users are interested in
- Conditional Rendering: Don't render Storyteller views that are far off-screen
- 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
Recommended Implementation Options#
- For Simple Lists (Recommended for < 10 items)
- Use
ScrollViewfor straightforward implementations -
Best for fixed, smaller lists of content
-
For Longer Lists (Recommended for 10+ items)
- Use FlashList
- Provides better performance and memory management
- 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}
/>
);
};