DEV Community

Alberto Cabrera
Alberto Cabrera

Posted on • Updated on

Create a TikTok-like Scrolling Video Feed with React Native Expo

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.

Demo

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
Enter fullscreen mode Exit fullscreen mode

2. FeedScreen component

You can name this component whatever you prefer.

import React from 'react'

const FeedScreen = () => {
 return (
   <>
   </>
 )
}

export default FeedScreen
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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",
];
Enter fullscreen mode Exit fullscreen mode

5. State Management and Viewability

const [currentViewableItemIndex, setCurrentViewableItemIndex] = useState(0);
const viewabilityConfig = { viewAreaCoveragePercentThreshold: 50 }
const viewabilityConfigCallbackPairs = useRef([{ viewabilityConfig, onViewableItemsChanged }])
Enter fullscreen mode Exit fullscreen mode

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);
    }
  }
Enter fullscreen mode Exit fullscreen mode

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>
  );
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

Item component detailed breakdown

const video = React.useRef<Video | null>(null);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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()}>
Enter fullscreen mode Exit fullscreen mode

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)}
/>
Enter fullscreen mode Exit fullscreen mode

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%',
  },
});
Enter fullscreen mode Exit fullscreen mode

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%',
  },
});
Enter fullscreen mode Exit fullscreen mode

That’s all! Happy coding 🚀

Top comments (2)

Collapse
 
lastreaction22 profile image
lastreaction

That's amazing! Let me try it for my TikTokToo

Collapse
 
ly168develop profile image
LyTran

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