Skip to main content

Fixing Movie Swiping and Fetching Issues

This document provides a detailed breakdown of the issues encountered with the movie swiping and fetching functionality, the root causes, and the solutions implemented to resolve them.

Problem 1: Re-swiping Previously Swiped Movies

The Issue

After swiping through a set of movies, previously swiped movies would reappear in the swipe stack, leading to a poor user experience.

The Root Cause

The useRandomMovies hook was responsible for fetching movies from the database, but it had no knowledge of the movies that had already been swiped. This meant that when the app fetched a new batch of movies, it could include movies that were already marked as "swiped" in the local state.

The Fix

The solution was to filter the movies returned from the API, removing any that were already present in the useSwipedMoviesStore. This was achieved by modifying the SwipeScreen component to use the filterUnswipedMovies function from the store.

Before:

// In daggh-companion/app/(tabs)/index.tsx

const movies = useMemo(() => {
const flattened = data?.pages.flatMap((page) => page.movies) || []
return flattened
}, [data])

After:

// In daggh-companion/app/(tabs)/index.tsx

import { useSwipedMoviesStore } from "@/store/useSwipedMoviesStore"

// ...

const { filterUnswipedMovies } = useSwipedMoviesStore()

const movies = useMemo(() => {
const flattened = data?.pages.flatMap((page) => page.movies) || []
return filterUnswipedMovies(flattened)
}, [data, filterUnswipedMovies])

Problem 2: Infinite Loop at the End of the Movie List

The Issue

After swiping through all available movies, the app would get stuck in a loop, repeatedly fetching and displaying the last page of movies.

The Root Cause

The getNextPageParam function in the useRandomMovies hook was not correctly identifying the end of the movie list. This caused the app to continuously request the next page, even when there were no more pages to fetch.

The Fix

The getNextPageParam function was updated to be more robust in detecting the end of the data. The new logic checks for three conditions:

  1. The lastPage.movies array is empty.
  2. The number of movies returned is less than the requested limit.
  3. The API returns a specific "end of data" message.

Before:

// In daggh-companion/app/hooks/useRandomMovies.ts

getNextPageParam: (lastPage, allPages) => {
// ... complex logic that was not robust enough
}

After:

// In daggh-companion/app/hooks/useRandomMovies.ts

getNextPageParam: (lastPage, allPages) => {
if (!lastPage.movies.length || lastPage.movies.length < limit) {
return undefined
}
return allPages.length + 1
}

Problem 3: The Race Condition with the Last Card

The Issue

Even with the improved fetching logic, the UI would not correctly display the "empty state" after the very last movie was swiped. The last card would reappear, creating a frustrating user experience.

The Root Cause

This was a subtle race condition between the optimistic UI update and the state update. When the last card was swiped, the app would optimistically remove it from the visual stack. However, the fact that the movie had been swiped was only recorded in the useSwipedMoviesStore after the backend confirmed the swipe. This created a small window of time where the cardStack was empty, but the movies list still contained the last movie, causing the UI to re-render with the last card.

The Fix

The solution was to make the optimistic update more complete by updating both the visual card stack and the persistent swiped movies store at the same time. This was achieved by modifying the onMutate and onError callbacks in the SwipeStack.tsx component.

Before:

// In daggh-companion/app/components/SwipeStack.tsx

onMutate: async (swipeData) => {
// ...
setCardStack((prev) => prev.slice(0, -1))
return { swipedMovie, swipedMovieId: tmdbId }
},
onError: (err, _variables, context) => {
// ...
if (context && context.swipedMovie) {
setCardStack((prev) => (context.swipedMovie ? [...prev, context.swipedMovie] : prev))
}
},

After:

// In daggh-companion/app/components/SwipeStack.tsx

onMutate: async (swipeData) => {
// ...
setCardStack((prev) => prev.slice(0, -1)) // Visually remove the card
addSwipedMovie(tmdbId) // Add to persistent swiped list
return { swipedMovie, swipedMovieId: tmdbId }
},
onError: (err, _variables, context) => {
// ...
if (context && context.swipedMovie) {
const swipedMovieId = context.swipedMovie.tmdb_id ?? context.swipedMovie.id
removeSwipedMovie(swipedMovieId) // Rollback optimistic add
setCardStack((prev) => (context.swipedMovie ? [...prev, context.swipedMovie] : prev))
}
},

Problem 4: The Broken UI After One Swipe

The Issue

After implementing the fix for the race condition, the UI would break after a single swipe. The card would not be visually removed from the stack, and the user would be unable to swipe any more cards.

The Root Cause

In the process of fixing the race condition, the line of code that visually removes the card from the stack (setCardStack) was accidentally removed. This meant that while the app was correctly marking the movie as "swiped" in the background, the card itself was never leaving the screen.

The Fix

The solution was to simply re-introduce the line of code that removes the card from the visual stack, ensuring that it happens at the same time the movie is added to the persistent swiped list.

Before:

// In daggh-companion/app/components/SwipeStack.tsx

onMutate: async (swipeData) => {
// ...
addSwipedMovie(tmdbId)
return { swipedMovie, swipedMovieId: tmdbId }
},

After:

// In daggh-companion/app/components/SwipeStack.tsx

onMutate: async (swipeData) => {
// ...
setCardStack((prev) => prev.slice(0, -1)) // Visually remove the card
addSwipedMovie(tmdbId) // Add to persistent swiped list
return { swipedMovie, swipedMovieId: tmdbId }
},