📋 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:
- The profile page already has the full watchlist from
getWatchlist() — by definition, every repo shown in the Watchlist section is being watched
- 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:
- Letting
WatchButton accept an optional initialIsWatched prop to skip the check API call when the parent already knows the watch state
- 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
💡 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
- Fork the repository
- Create a branch:
git checkout -b fix/issue-35-watchbutton-n-plus-1
- Update
frontend/components/watch-button.tsx
- Update the
Watchlist component in frontend/app/(auth)/profile/page.tsx
- Open DevTools, count network calls before and after
- Open a PR with Network waterfall screenshots showing the reduction!
📋 Description
On the Profile page, the Watchlist section renders one
<WatchButton>per watched repository. EachWatchButtonindependently callscheckIsWatched(token, owner, name)in auseEffecton 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:
getWatchlist()— by definition, every repo shown in the Watchlist section is being watchedWatchButtoncomponent 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:
WatchButtonaccept an optionalinitialIsWatchedprop to skip the check API call when the parent already knows the watch stateWatchlistcomponent inprofile/page.tsx📍 Files to Change
frontend/components/watch-button.tsx— addinitialIsWatched?: booleanpropfrontend/app/(auth)/profile/page.tsx— passinitialIsWatched={true}for watchlist items🔍 Current Broken Code
✅ What To Do
Step 1 — Update
WatchButtonto accept an optionalinitialIsWatchedprop:Step 2 — Pass
initialIsWatched={true}inprofile/page.tsx: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
onUnwatchcallback:🏁 Acceptance Criteria
WatchButtonaccepts aninitialIsWatched?: booleanpropinitialIsWatchedis provided, nocheckIsWatchedAPI call is fired on mountinitialIsWatched={true}for all watchlist itemsinitialIsWatchedprop → still fires the check)💡 Technical Hints
initialIsWatched === undefined(not!initialIsWatched) is the correct check —falseis a valid value meaning "we know it's not watched, don't fetch"/watchlist/check. Before this fix: N requests. After: 0 requestsinitialIsWatchedprop should not affect subsequenttoggleWatchcalls — only the initial fetch. The localisWatchedstate still updates correctly after each toggle🚀 Getting Started
git checkout -b fix/issue-35-watchbutton-n-plus-1frontend/components/watch-button.tsxWatchlistcomponent infrontend/app/(auth)/profile/page.tsx