iiitl / iiitl/Opensource_Compass
Fix N+1 API Requests in WatchButton on Profile Watchlist
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 16
- PR merge metrics
- No merged PRs in 30d
Description
### π Description
On the Profile page, the Watchlist section renders one `` 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
```tsx
// 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]);
```
```tsx
// profile/page.tsx β Watchlist renders WatchButton with NO hint that repo is already watched
// β 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:**
```tsx
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`:**
```tsx
// In the Watchlist component, every item in `repos` is by definition watched:
```
**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:
```tsx
// 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
}
```
```tsx
// In profile/page.tsx Watchlist component:
const [repos, setRepos] = useState([]);
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!
Contributor guide
Research direction
Start in frontend/components/watch-button.tsx by reading the mount effect and its existing toggle behavior, then inspect the Watchlist rendering in frontend/app/(auth)/profile/page.tsx. Verify in the browser Network tab that profile loading no longer sends per-repository check requests, while Discover still checks normally; the optional bonus is immediate card removal after unwatching.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, performance
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100