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.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!
Runnable Demo
You can try running this code via Expo.
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,
},
});
Top comments (0)