Updated March 10, 2021
Vertical and Horizontal Scrolling in a SectionList/FlatList
I use Spotify a lot. In the mobile app the home screen allows you to scroll both vertically (across different groups) and horizontally (within a group). Here's how I do the same in React Native.
Below is a demo of what we'll end up with. It allows you to render a section's data either horizontally or vertically.
This is leveraging a few advanced props of FlatList and SectionList. If you're looking to brush up on the basics checkout this intro to the FlatList component.
Starting Code
The following code allows you to render a standard list of sections (all vertical).
App.js
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import {
StyleSheet,
Text,
View,
SectionList,
SafeAreaView,
Image,
} from 'react-native';
const ListItem = ({ item }) => {
return (
<View style={styles.item}>
<Image
source={{
uri: item.uri,
}}
style={styles.itemPhoto}
resizeMode="cover"
/>
<Text style={styles.itemText}>{item.text}</Text>
</View>
);
};
export default () => {
return (
<View style={styles.container}>
<StatusBar style="light" />
<SafeAreaView style={{ flex: 1 }}>
<SectionList
contentContainerStyle={{ paddingHorizontal: 10 }}
stickySectionHeadersEnabled={false}
sections={SECTIONS}
renderSectionHeader={({ section }) => (
<Text style={styles.sectionHeader}>{section.title}</Text>
)}
renderItem={({ item, section }) => {
return <ListItem item={item} />;
}}
/>
</SafeAreaView>
</View>
);
};
const SECTIONS = [
{
title: 'Made for you',
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/10/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1002/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1006/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1008/200',
},
],
},
{
title: 'Punk and hardcore',
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1011/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/1012/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1013/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1015/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1016/200',
},
],
},
{
title: 'Based on your recent listening',
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1020/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/1024/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1027/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1035/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1038/200',
},
],
},
];
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#121212',
},
sectionHeader: {
fontWeight: '800',
fontSize: 18,
color: '#f4f4f4',
marginTop: 20,
marginBottom: 5,
},
item: {
margin: 10,
},
itemPhoto: {
width: 200,
height: 200,
},
itemText: {
color: 'rgba(255, 255, 255, 0.5)',
marginTop: 5,
},
});
Rendering Horizontal List
First thing we'll do is render a FlatList
inside of the renderSectionHeader
function. We have access to all of the section's data here so we can just forward that along to the FlatList
. We'll also tell this FlatList to render horizontally.
<SectionList
// ...
renderSectionHeader={({ section }) => (
<>
<Text style={styles.sectionHeader}>{section.title}</Text>
<FlatList
horizontal
data={section.data}
renderItem={({ item }) => <ListItem item={item} />}
showsHorizontalScrollIndicator={false}
/>
</>
)}
/>
The problem with just doing this is that we render the section's data both horizontally and vertically. Therefore we need to disable the renderItem
function.
<SectionList
// ...
renderSectionHeader={({ section }) => (
<>
<Text style={styles.sectionHeader}>{section.title}</Text>
<FlatList
horizontal
data={section.data}
renderItem={({ item }) => <ListItem item={item} />}
showsHorizontalScrollIndicator={false}
/>
</>
)}
renderItem={({ item, section }) => {
return null;
// return <ListItem item={item} />;
}}
/>
That solves the duplicate data problem but now we can only show data horizontally - negating the value of using a SectionList
. Instead, let's go ahead and add a property to specify when to render data horizontally.
If the section does not specify that the data should be rendered horizontally then we'll just render it vertically.
<SectionList
// ...
renderSectionHeader={({ section }) => (
<>
<Text style={styles.sectionHeader}>{section.title}</Text>
{section.horizontal ? (
<FlatList
horizontal
data={section.data}
renderItem={({ item }) => <ListItem item={item} />}
showsHorizontalScrollIndicator={false}
/>
) : null}
</>
)}
renderItem={({ item, section }) => {
if (section.horizontal) {
return null;
}
return <ListItem item={item} />;
}}
/>
const SECTIONS = [
{
title: 'Made for you',
horizontal: true,
data: [
// ...
],
},
// ...
];
And there you have it!
Want to go even further? Try making an infinite scrolling list that does the same!
Finished Code
App.js
import { StatusBar } from 'expo-status-bar';
import React from 'react';
import {
StyleSheet,
Text,
View,
SectionList,
SafeAreaView,
Image,
FlatList,
} from 'react-native';
const ListItem = ({ item }) => {
return (
<View style={styles.item}>
<Image
source={{
uri: item.uri,
}}
style={styles.itemPhoto}
resizeMode="cover"
/>
<Text style={styles.itemText}>{item.text}</Text>
</View>
);
};
export default () => {
return (
<View style={styles.container}>
<StatusBar style="light" />
<SafeAreaView style={{ flex: 1 }}>
<SectionList
contentContainerStyle={{ paddingHorizontal: 10 }}
stickySectionHeadersEnabled={false}
sections={SECTIONS}
renderSectionHeader={({ section }) => (
<>
<Text style={styles.sectionHeader}>{section.title}</Text>
{section.horizontal ? (
<FlatList
horizontal
data={section.data}
renderItem={({ item }) => <ListItem item={item} />}
showsHorizontalScrollIndicator={false}
/>
) : null}
</>
)}
renderItem={({ item, section }) => {
if (section.horizontal) {
return null;
}
return <ListItem item={item} />;
}}
/>
</SafeAreaView>
</View>
);
};
const SECTIONS = [
{
title: 'Made for you',
horizontal: true,
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/10/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1002/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1006/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1008/200',
},
],
},
{
title: 'Punk and hardcore',
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1011/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/1012/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1013/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1015/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1016/200',
},
],
},
{
title: 'Based on your recent listening',
data: [
{
key: '1',
text: 'Item text 1',
uri: 'https://picsum.photos/id/1020/200',
},
{
key: '2',
text: 'Item text 2',
uri: 'https://picsum.photos/id/1024/200',
},
{
key: '3',
text: 'Item text 3',
uri: 'https://picsum.photos/id/1027/200',
},
{
key: '4',
text: 'Item text 4',
uri: 'https://picsum.photos/id/1035/200',
},
{
key: '5',
text: 'Item text 5',
uri: 'https://picsum.photos/id/1038/200',
},
],
},
];
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#121212',
},
sectionHeader: {
fontWeight: '800',
fontSize: 18,
color: '#f4f4f4',
marginTop: 20,
marginBottom: 5,
},
item: {
margin: 10,
},
itemPhoto: {
width: 200,
height: 200,
},
itemText: {
color: 'rgba(255, 255, 255, 0.5)',
marginTop: 5,
},
});