Vertical and Horizontal Scrolling in a SectionList/FlatList

Spencer Carli
React Native School
5 min readMar 17, 2021

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.

Starting Code

The following code allows you to render a standard list of sections (all vertical).

// App.jsimport { 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!

Runnable Demo

You can try running this code via Expo.

Finished Code

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,
},
});

Originally published at https://www.reactnativeschool.com.

--

--