Create a TikTok-like Scrolling Video Feed with React Native Expo
Tags: reactnative, expo, mobile, react
Published: 2023-12-24 18:23:34

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/md16yfgtorx80pk79g6w.gif)

**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](https://gist.github.com/jsturgis/3b19447b304616f18657).
```
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 🚀