Before we dive into the article, let's preview a demonstration video of the Tiktok-esque video feed that we are about to build. The demo video shows a feed with smooth scrolling, auto-play and pause functionality. Pay attention to how videos automatically play when in focus and pause when not, and the full-screen playback for an immersive experience.
Prerequisite: Ensure that you have a React Native Expo project already set up. This is a fundamental requirement for following along with the tutorial.
1. Install expo-av
expo-av
is an Expo module specifically designed for audio and video playback in React Native Expo apps. It's vital for our project because it allows for smooth video playback and control, ensuring an engaging user experience on both iOS and Android platforms.
To set up, simply run
npx expo install expo-av
2. FeedScreen component
You can name this component whatever you prefer.
import React from 'react'
const FeedScreen = () => {
return (
<>
</>
)
}
export default FeedScreen
3. Import Modules
import { View, Dimensions, FlatList, StyleSheet, Pressable } from 'react-native';
import { Video, ResizeMode } from 'expo-av';
import React, { useEffect, useRef, useState } from 'react';
Video
, ResizeMode
from expo-av
:
These will handle video playback. Video is the component for displaying videos, and ResizeMode controls how the video fits into its container.
4. Video Data Array
You can copy the videos array below or copy from this list of public video urls here.
const videos = [
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
];
5. State Management and Viewability
const [currentViewableItemIndex, setCurrentViewableItemIndex] = useState(0);
const viewabilityConfig = { viewAreaCoveragePercentThreshold: 50 }
const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])
currentViewableItemIndex
Keeps track of which video is currently in the user's view. This is crucial for knowing which video to play.
viewabilityConfig
Defines what counts as a "viewable" item. Here, an item covering more than 50% of the screen is considered viewable. This threshold ensures that the video in the main focus of the screen is the one that plays.
viewabilityConfigCallbackPairs
Manages how videos in the feed are played and paused based on their visibility on the screen.
6. Viewable Items Change Handler
const onViewableItemsChanged = ({ viewableItems }: any) => {
if (viewableItems.length > 0) {
setCurrentViewableItemIndex(viewableItems[0].index ?? 0);
}
}
This function updates currentViewableItemIndex based on the item currently in view. It's essential for determining which video should be playing as the user scrolls.
7. FlatList for Rendering Videos
return (
<View style={styles.container}>
<FlatList
data={videos}
renderItem={({ item, index }) => (
<Item item={item} shouldPlay={index === currentViewableItemIndex} />
)}
keyExtractor={item => item}
pagingEnabled
horizontal={false}
showsVerticalScrollIndicator={false}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
/>
</View>
);
pagingEnabled
When set to true, the list allows snapping to items (paging) as you scroll, creating a carousel-like effect. This is similar to how feeds work in social media apps.
viewabilityConfigCallbackPairs
This prop links our viewability configuration and callback function to the FlatList. It's crucial for detecting which video is in the viewport and should thus be playing.
8. The Item Component
const Item = ({ item, shouldPlay }: {shouldPlay: boolean; item: string}) => {
const video = React.useRef<Video | null>(null);
const [status, setStatus] = useState<any>(null);
useEffect(() => {
if (!video.current) return;
if (shouldPlay) {
video.current.playAsync()
} else {
video.current.pauseAsync()
video.current.setPositionAsync(0)
}
}, [shouldPlay])
return (
<Pressable onPress={() => status.isPlaying ? video.current?.pauseAsync() : video.current?.playAsync()}>
<View style={styles.videoContainer}>
<Video
ref={video}
source={{ uri: item }}
style={styles.video}
isLooping
resizeMode={ResizeMode.COVER}
useNativeControls={false}
onPlaybackStatusUpdate={status => setStatus(() => status)}
/>
</View>
</Pressable>
);
}
Item component detailed breakdown
const video = React.useRef<Video | null>(null);
This reference allows you to control the video's playback programmatically (like playing or pausing). useRef
is used instead of a state variable because we need a way to persistently access the video component without causing re-renders.
const [status, setStatus] = useState<any>(null);
This state holds the playback status of the video (like whether it's playing, paused, buffering, etc.). It's updated whenever the playback status of the video changes.
useEffect(() => {
if (!video.current) return;
if (shouldPlay) {
video.current.playAsync();
} else {
video.current.pauseAsync();
video.current.setPositionAsync(0);
}
}, [shouldPlay]);
This useEffect is triggered whenever the shouldPlay prop changes. It checks if the video is supposed to be playing. If so, it starts playback; otherwise, it pauses the video and resets its position to the start.
<Pressable onPress={() => status.isPlaying ? video.current?.pauseAsync() : video.current?.playAsync()}>
Wraps the video component to make it interactive. When the user taps the video, it toggles between playing and pausing.
<Video
ref={video}
source={{ uri: item }}
style={styles.video}
isLooping
resizeMode={ResizeMode.COVER}
useNativeControls={false}
onPlaybackStatusUpdate={status => setStatus(() => status)}
/>
isLooping
: If true, the video will loop continuously.
resizeMode
: Determines how the video fits within the bounds of its container.
useNativeControls
: Set to false to hide native playback controls.
onPlaybackStatusUpdate
: A callback function that updates the status state whenever the playback status of the video changes.
9. Styles
const styles = StyleSheet.create({
container: {
flex: 1,
},
videoContainer: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
video: {
width: '100%',
height: '100%',
},
});
Full Code
import { View, Dimensions, FlatList, StyleSheet, Pressable } from 'react-native';
import { Video, ResizeMode } from 'expo-av';
import React, { useEffect, useRef, useState } from 'react';
const videos = [
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4",
"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4",
];
export default function FeedScreen() {
const [currentViewableItemIndex, setCurrentViewableItemIndex] = useState(0);
const viewabilityConfig = { viewAreaCoveragePercentThreshold: 50 }
const onViewableItemsChanged = ({ viewableItems }: any) => {
if (viewableItems.length > 0) {
setCurrentViewableItemIndex(viewableItems[0].index ?? 0);
}
}
const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])
return (
<View style={styles.container}>
<FlatList
data={videos}
renderItem={({ item, index }) => (
<Item item={item} shouldPlay={index === currentViewableItemIndex} />
)}
keyExtractor={item => item}
pagingEnabled
horizontal={false}
showsVerticalScrollIndicator={false}
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current}
/>
</View>
);
}
const Item = ({ item, shouldPlay }: {shouldPlay: boolean; item: string}) => {
const video = React.useRef<Video | null>(null);
const [status, setStatus] = useState<any>(null);
useEffect(() => {
if (!video.current) return;
if (shouldPlay) {
video.current.playAsync()
} else {
video.current.pauseAsync()
video.current.setPositionAsync(0)
}
}, [shouldPlay])
return (
<Pressable onPress={() => status.isPlaying ? video.current?.pauseAsync() : video.current?.playAsync()}>
<View style={styles.videoContainer}>
<Video
ref={video}
source={{ uri: item }}
style={styles.video}
isLooping
resizeMode={ResizeMode.COVER}
useNativeControls={false}
onPlaybackStatusUpdate={status => setStatus(() => status)}
/>
</View>
</Pressable>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
videoContainer: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
},
video: {
width: '100%',
height: '100%',
},
});
That’s all! Happy coding 🚀
Top comments (2)
Great tutorial! Worked flawlessly!
So great! but i have a problem with that.
I got an error when the number of videos is over 20 then it doesn't display the video.
1->12 video is display
12 -> 20 video isn't display.
Could you explain for me with that? Thanks
Some comments may only be visible to logged-in visitors. Sign in to view all comments.