iiitl / iiitl/Opensource_Compass

Fix N+1 API Requests in WatchButton on Profile Watchlist

Open Beginner friendly
#57 2 comments 0 reactions 0 assignees View on GitHub
backend frontend intermediate performance
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.