Reference solution
A strong review should identify the following issues, in roughly this priority order.
Findings
1. Repeated intersections can request the same cursor concurrently
Location: src/use-activity-feed.ts:28-34 and src/use-activity-feed.ts:37-43
The observer callback starts a request whenever an intersecting entry arrives, but it does not claim the current cursor or check whether that cursor already has an in-flight request. Intersection observers can invoke the callback repeatedly before loadPage resolves and advances nextCursor, producing duplicate requests and duplicate state transitions.
Track the cursor being loaded, ignore further observations for that cursor, and release the claim on failure so a later intersection can retry.
2. Every render leaves another active intersection observer
Location: src/use-activity-feed.ts:36-50
The hook constructs an observer during rendering. There is no effect cleanup to disconnect it when the sentinel changes, after a rerender, or when the feed unmounts. Old observers continue holding callbacks and can initiate additional loads, so requests and retained resources grow over a long session.
Create the observer in an effect tied to the current sentinel and stable callback dependencies, and return cleanup that unobserves or disconnects it.
3. Live and historical delivery can render the same activity twice
Location: src/use-activity-feed.ts:31-33 and src/use-activity-feed.ts:52-54
Both merge paths concatenate arrays without comparing stable activity IDs. If a live event is also included in the next historical page, or is delivered more than once, the feed contains duplicate rows and its ordering becomes misleading.
Merge through a collection keyed by activity ID, preserving live items at the top and newly fetched older items at the bottom.
4. The feed mounts its entire accumulated history
Location: src/activity-feed.tsx:28-36
The component maps every retained item even though the contract limits the mounted window to 100 rows. As the tab stays open, DOM nodes, image elements, and reconciliation work grow without bound.
Keep the complete history in state if it is needed for navigation, but derive and render a window of at most 100 rows, with an explicit policy for shifting that window as the user scrolls.
5. Offscreen thumbnails are requested eagerly
Location: src/activity-row.tsx:14-20
Every thumbnail has loading="eager". Historical pages therefore start fetching images far outside the viewport, competing with the current feed for bandwidth.
Use lazy loading for feed thumbnails, reserving eager loading or explicit preload for the first image that is actually visible.
Reasonable non-findings
- Keeping all fetched activities in memory is allowed; the requirement bounds mounted rows, not the backing collection.
- Advancing the cursor only after a successful response is correct. The missing piece is an in-flight claim before awaiting that response.
- Prepending live items and appending historical items is the intended ordering when the collections do not overlap.
- A single sentinel at the end of the rendered list is sufficient; no second observer is required at the top.