Published on December 22, 2025

An extra-sloppy TikTok-style video feed in React Native

Dave KissJoshua Alphonse
By Dave and Joshua8 min readEngineering

At Mux, we often hear from mobile developers asking the same question: "I'm building an app like TikTok with infinite video scrolling. How do I do that with Mux?"

It's a fair question. Short-form vertical video feeds are everywhere now (think TikTok, Instagram Reels, YouTube Shorts) and they all work the same way: videos play instantly and there's not a buffering spinner in sight.

As is often the story (no pun intended) with video, there be dragons in the details; recreating that buttery-smooth experience is a weeeee bit harder than it looks.

LinkOne does not simply "just play a video"

When users scroll through TikTok, they don't care about what's happening behind the scenes. They just expect it to work. MOAR VIDEOZ. The next video should be ready the moment they swipe, scrolling back shouldn't re-download content, and the whole thing needs to stay smooth on a five-year-old phone.

Building this yourself means solving several problems that all relate:

  • Preloading: How do you load the next video before the user gets there so it shows up instantly?
  • Memory management: How do you keep performance smooth when there are dozens of videos in the feed?
  • Cost optimization: How do you avoid burning through bandwidth (and your video delivery budget) by loading videos the user never watches?
  • Playback control: How do you make sure only the visible video is playing?

We've answered these questions enough times that we decided to build something to show rather than just tell.

LinkIntroducing Slop Social

You've probably heard people complaining about "AI slop" (you may even be one of them!)—the flood of low-effort AI-generated content clogging up the internet. We decided to lean in. You want slop? We got slop. Come get it.

Slop Social is a demo React Native app where you doom-scroll through AI-generated videos of people making messes. Paint cans tipping over, dogs tracking mud everywhere, kids knocking over grocery store displays, plates hitting the floor, trucks stuck in mud. Yeah, actual slop.

Unfortunately, I showed this feed to my 5-year-old daughter and she was enthralled. We’re in trouble.

View the Slop Social repository on GitHub →

No user accounts or backend complexity here, we're just focusing on the feed. The app demonstrates:

  • Full-screen vertical video playback using Mux
  • Smooth swipe navigation between videos
  • Preloading that minimizes both latency and cost
  • Memory-optimized rendering that won't crash on older devices
  • Gesture controls (tap to pause, double-tap to like)

LinkHow the feed stays instant

Building a smooth video feed requires three systems working together: a virtualized list that recycles components efficiently, a preloading strategy that anticipates where the user is headed, and playback controls that ensure only one video plays at a time.

Here's how these pieces fit together in Slop Social:

Breakdown
VideoFeed ├─ FlashList (virtualized scrolling) │ ├─ pagingEnabled + snap behavior │ └─ VideoItem (recycled per-video container) │ ├─ VideoView + useVideoPlayer (expo-video) │ ├─ Overlay UI (likes, comments, etc.) │ └─ GestureDetector (tap to pause, double-tap to like) ├─ Scroll tracking (currentIndex + direction) └─ Preload window (determines which videos get a source)

FlashList handles the scrolling and component recycling. Instead of rendering all the videos at once, FlashList only keeps a handful of components mounted and recycles them as items scroll in and out of view.

Scroll tracking watches where the user is in the feed and which direction they're moving. This feeds into preloading decisions so we only buffer videos ahead of the user, not behind them.

When videos are inside the preload window, they get a source URL and start buffering. Videos outside the window get null as their source, which frees up the memory. As the user scrolls, this window slides along with them.

Combined, this approach will make sure the videos load before the user reaches them, there’s no memory concerns regardless of feed length, and you're not paying to deliver videos no one watches.

Let's look at each piece in detail.

LinkWhy FlashList over FlatList?

React Native's FlatList works for many use cases, but Shopify's FlashList offers meaningful advantages for video feeds, especially on lower-end devices.

The big difference is recycling. FlatList creates and destroys components as they enter and leave the viewport. FlashList recycles them: when a video scrolls off-screen, the component is re-rendered with the next video's props. This means fewer allocations, less garbage collection pressure, and smoother scrolling.

Benchmarks on low-end Android devices show FlashList achieving up to 5x better UI thread FPS and 10x better JS thread FPS compared to FlatList.

Link1. Configure FlashList for video

jsx
<FlashList data={videos} renderItem={renderItem} pagingEnabled snapToInterval={SCREEN_HEIGHT} snapToAlignment="start" decelerationRate="fast" overrideItemLayout={(layout, _item, index) => { layout.size = SCREEN_HEIGHT; layout.offset = SCREEN_HEIGHT * index; }} drawDistance={SCREEN_HEIGHT * 3} getItemType={() => "video"} />

Let's break down what the FlashList-specific props do:

  • getItemType={() => "video"}: Tells FlashList that all items are the same type. This creates a single recycling pool, so video components are always recycled into other video components without wasting re-renders from type mismatches.
  • drawDistance={SCREEN_HEIGHT * 3}: Controls how far ahead (in pixels) FlashList renders off-screen content. Setting this to 3x the screen height means FlashList will render videos that are up to three screens away, giving the preloading logic time to buffer content before the user gets there.
  • overrideItemLayout: Pre-calculates item sizes so FlashList knows exactly where each item will be positioned. This eliminates layout thrashing so there’s no jumping or repositioning as items render. For a full-screen feed, every item is exactly SCREEN_HEIGHT tall.

One gotcha with FlashList: avoid key props inside your item components. Unlike FlatList, keys break FlashList's recycling and kill performance. The recycling system needs stable component trees to reuse efficiently.

Link2. Directional preloading

Rather than preloading a fixed number of videos in both directions, Slop Social preloads based on scroll direction:

  • 5 videos ahead in the direction the user is scrolling (for HLS manifest/segment prefetch)
  • 1 video behind for quick back-scroll

Videos outside this window get their source set to null, freeing memory while keeping the component mounted for fast recycling:

jsx
const MAX_PRELOAD_DISTANCE = 5; const renderItem = useCallback(({ item, index }) => { const isActive = index === currentIndex; // Calculate preload based on scroll direction const distanceFromActive = index - currentIndex; const isAhead = scrollDirection === 'down' ? distanceFromActive > 0 : distanceFromActive < 0; const shouldPreloadAhead = isAhead && Math.abs(distanceFromActive) <= MAX_PRELOAD_DISTANCE; const shouldPreloadBehind = !isAhead && Math.abs(distanceFromActive) === 1; const shouldPreload = shouldPreloadAhead || shouldPreloadBehind; return ( <VideoItem video={item} isActive={isActive} shouldPreload={shouldPreload} /> ); }, [currentIndex, scrollDirection]);

Inside the VideoItem component, we use expo-video's useVideoPlayer hook with conditional source loading. When shouldPreload or isActive is true, we pass the video URL. Otherwise, we pass null:

useVideoPlayer
const player = useVideoPlayer( shouldPreload || isActive ? videoUrl : null, (p) => { p.loop = true; p.muted = true; // Start paused - setting source triggers HLS manifest/segment prefetch } );

With this approach, expo-video starts buffering as soon as a VideoPlayer receives a source, even before it's connected to a visible VideoView. By the time the user swipes to the next video, it's already loaded and ready to play instantly.

Link3. Play only the active video

Every other video in the feed should be paused. This is both a UX requirement (you don't want audio from off-screen videos) and a cost optimization:

jsx
useEffect(() => { if (!player) return; if (isActive && !paused) { // Active and playing: unmute and play player.muted = false; player.play(); } else if (isActive && paused) { // Active but user paused (tapped to pause) player.muted = true; player.pause(); } else if (shouldPreload) { // Preloading: keep paused at start, HLS will still prefetch player.muted = true; player.pause(); player.currentTime = 0; } else { // Not active, not preloading: fully stop player.muted = true; player.pause(); } // Reset to start when becoming active if (isActive && !wasActiveRef.current) { player.currentTime = 0; } wasActiveRef.current = isActive; }, [isActive, paused, player, shouldPreload]);

Notice that preloading videos are paused but still have their source set. This triggers HLS manifest and segment prefetch without actually playing audio or consuming significant resources.

With Mux, you're billed for delivery when video segments are actually downloaded. By pausing off-screen videos, you're not wasting bandwidth on content the user isn't watching. Noice. Go treat yourself with some extra cash in your pocket.

LinkCost optimization matters

When you're streaming video at scale, delivery costs add up. A naive implementation might preload 10 videos ahead "just in case", keep playing videos off-screen, or re-download videos when scrolling back. Not great, Bob.

The videos might be a mess, but the implementation shouldn't be. Slop Social takes a different approach:

  1. Directional preloading: We preload 5 videos ahead in the scroll direction and just 1 behind—not the entire feed in both directions
  2. Null sources outside the window: Videos outside the preload window have their source set to null, freeing memory entirely
  3. Pause aggressively: Any video that's not currently visible is paused and muted
  4. Cache intelligently: When users scroll back to a previously-watched video, we serve it from cache rather than re-fetching

This keeps playback fast without burning through extra delivery costs.

LinkWhat we skipped (for now)

Slop Social doesn't handle video uploads or content moderation. Those are their own problems, each worth a separate post. We wanted to isolate the playback and scrolling piece first, since that's where most developers get stuck.

The repo is open source. Fork it, break it, make a mess.

Written By

Dave Kiss

Dave Kiss – Senior Community Engineering Lead

Was: solo-developreneur. Now: developer community person. Happy to ride a bike, hike a hike, high-five a hand, and listen to spa music.

Joshua Alphonse

Joshua Alphonse – Community Engineer

A Long Island native of Caribbean heritage, and a software engineer with a passion for record collecting and creative coding.

Leave your wallet where it is

No credit card required to get started.