Skip to content

Fix N+1 API Requests in WatchButton on Profile Watchlist #57

@Vedant1703

Description

@Vedant1703

📋 Description

On the Profile page, the Watchlist section renders one <WatchButton> per watched repository. Each WatchButton independently calls checkIsWatched(token, owner, name) in a useEffect on mount to determine if the repo is currently being watched.

This creates an N+1 request problem: if a user has 10 repos in their watchlist, the profile page fires 10 separate GET /watchlist/check?owner=...&name=... API requests simultaneously on load — one for every card.

This is wasteful and unnecessary, because:

  1. The profile page already has the full watchlist from getWatchlist() — by definition, every repo shown in the Watchlist section is being watched
  2. The same WatchButton component is also used on the Discover page, where the check actually makes sense (the user may or may not be watching any given discovered repo)

The fix requires:

  1. Letting WatchButton accept an optional initialIsWatched prop to skip the check API call when the parent already knows the watch state
  2. Passing that initial state from the Watchlist component in profile/page.tsx

📍 Files to Change

  • frontend/components/watch-button.tsx — add initialIsWatched?: boolean prop
  • frontend/app/(auth)/profile/page.tsx — pass initialIsWatched={true} for watchlist items

🔍 Current Broken Code

// watch-button.tsx — runs an API call FOR EVERY instance on mount
useEffect(() => {
  if (token) {
    checkIsWatched(token, owner, name)   // ← N separate requests for N cards
      .then(setIsWatched)
      .catch(() => setIsWatched(false))
      .finally(() => setLoading(false));
  }
}, [token, owner, name]);
// profile/page.tsx — Watchlist renders WatchButton with NO hint that repo is already watched
<WatchButton owner={repo.repo_owner} name={repo.repo_name} />
// ↑ causes a redundant API call even though we KNOW it's watched

✅ What To Do

Step 1 — Update WatchButton to accept an optional initialIsWatched prop:

interface WatchButtonProps {
  owner: string;
  name: string;
  initialIsWatched?: boolean;  // ← add this
}

export function WatchButton({ owner, name, initialIsWatched }: WatchButtonProps) {
  const { token } = useAuth();
  const [isWatched, setIsWatched] = useState(initialIsWatched ?? false);
  const [loading, setLoading] = useState(initialIsWatched === undefined); // only show loading if we need to fetch

  useEffect(() => {
    // Skip the API call if the parent gave us the initial state
    if (initialIsWatched !== undefined) {
      setLoading(false);
      return;
    }
    if (token) {
      checkIsWatched(token, owner, name)
        .then(setIsWatched)
        .catch(() => setIsWatched(false))
        .finally(() => setLoading(false));
    }
  }, [token, owner, name, initialIsWatched]);

  // ... rest unchanged
}

Step 2 — Pass initialIsWatched={true} in profile/page.tsx:

// In the Watchlist component, every item in `repos` is by definition watched:
<WatchButton
  owner={repo.repo_owner}
  name={repo.repo_name}
  initialIsWatched={true}   // ← add this, skips the check API call
/>

Bonus — Remove items optimistically from the watchlist UI when unwatched:

Right now, if a user clicks "Unwatch" on the Profile page, the button state changes but the card stays visible until the next page reload. Improve this by passing an optional onUnwatch callback:

// In WatchButton:
interface WatchButtonProps {
  owner: string;
  name: string;
  initialIsWatched?: boolean;
  onUnwatch?: () => void;   // ← optional callback
}

// In toggleWatch, after successfully removing:
if (isWatched) {
  await removeFromWatchlist(token, owner, name);
  setIsWatched(false);
  toast.success("Removed from watchlist");
  onUnwatch?.();   // ← notify parent to remove card
}
// In profile/page.tsx Watchlist component:
const [repos, setRepos] = useState<WatchedRepo[]>([]);

<WatchButton
  owner={repo.repo_owner}
  name={repo.repo_name}
  initialIsWatched={true}
  onUnwatch={() => setRepos(prev => prev.filter(r => r.id !== repo.id))}
/>

🏁 Acceptance Criteria

  • WatchButton accepts an initialIsWatched?: boolean prop
  • When initialIsWatched is provided, no checkIsWatched API call is fired on mount
  • The Profile page passes initialIsWatched={true} for all watchlist items
  • The Discover page continues to work as before (no initialIsWatched prop → still fires the check)
  • Opening the Profile page with 10 watched repos fires 1 API call (for the watchlist) instead of 11 (1 + 10 checks)
  • (Bonus) Unwatching a repo on the Profile page removes the card immediately without a page reload

💡 Technical Hints

  • initialIsWatched === undefined (not !initialIsWatched) is the correct check — false is a valid value meaning "we know it's not watched, don't fetch"
  • Use browser DevTools → Network tab to confirm: open the Profile page and count how many requests match /watchlist/check. Before this fix: N requests. After: 0 requests
  • The initialIsWatched prop should not affect subsequent toggleWatch calls — only the initial fetch. The local isWatched state still updates correctly after each toggle

🚀 Getting Started

  1. Fork the repository
  2. Create a branch: git checkout -b fix/issue-35-watchbutton-n-plus-1
  3. Update frontend/components/watch-button.tsx
  4. Update the Watchlist component in frontend/app/(auth)/profile/page.tsx
  5. Open DevTools, count network calls before and after
  6. Open a PR with Network waterfall screenshots showing the reduction!

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions