Skip to content

fix: add pagination and infinite scrolling for feature requests and roadmap (#5) - #17

Merged
lexrus merged 2 commits into
mainfrom
fix/issue-5-feature-requests-pagination
Sep 5, 2026
Merged

fix: add pagination and infinite scrolling for feature requests and roadmap (#5)#17
lexrus merged 2 commits into
mainfrom
fix/issue-5-feature-requests-pagination

Conversation

@lexrus

@lexrus lexrus commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Description

Closes #5

Both FeatureRequestsScreen and RoadmapBoardScreen previously issued a single fetch request capped at 50 / 100 items with no mechanism to load subsequent pages, causing any items beyond the limit to be silently truncated.

This PR introduces infinite scrolling and pagination support across the SDK, backed by a reusable useFeatureRequests hook, footer loading indicators, filter/query reset behavior, and complete translations across all 14 supported locales.


Key Changes

  1. FeedbackClient.fetchFeatureRequests:

    • Expanded query parameters to accept limit, offset, columnId, and status.
    • Documented the pagination query parameters in AGENTS.md and README.md.
  2. Reusable Pagination Hook (useFeatureRequests):

    • Manages pagination lifecycle state (items, total, hasMore, isLoading, isRefreshing, isLoadingMore).
    • Supports incremental loadMore() with automatic page offset calculation.
    • Automatically restarts at page 0 on pull-to-refresh or query/filter option changes.
    • Handles deduplication by item ID when server response windows overlap.
    • Provides safe cancellation via AbortController on unmount or subsequent calls.
    • Provides applyItemChange() to update items (such as optimistic upvoting) in-place without triggering full re-fetches.
    • Exported from the public SDK surface in src/index.ts.
  3. FeatureRequestsScreen:

    • Integrated useFeatureRequests hook.
    • Connected onEndReached (onEndReachedThreshold={0.4}) for smooth infinite scroll.
    • Added footer loading spinner displaying localized loadingMore string.
    • Search query and version filter changes cleanly reset pagination back to page 0.
    • Pull-to-refresh cleanly re-fetches starting from page 0.
  4. RoadmapBoardScreen:

    • Connected onEndReached infinite scrolling per column.
    • Added a footer indicator and an explicit "Showing N of M" count affordance (showingCount(shown, total)) with a "Load more" manual trigger.
  5. Internationalization (All 14 Locales):

    • Added loadingMore to common, featureRequests, and roadmap.
    • Added loadMore and showingCount to roadmap.
    • Added localized translations across all 14 supported languages:
      • English (en)
      • Simplified Chinese (zhHans)
      • Traditional Chinese (zhHant)
      • Japanese (ja)
      • Korean (ko)
      • German (de)
      • Spanish (es)
      • French (fr)
      • Italian (it)
      • Portuguese (pt)
      • Polish (pl)
      • Norwegian (no)
      • Turkish (tr)
      • Vietnamese (vi)
  6. Test Coverage:

    • Added test/pagination.test.ts with 7 comprehensive test cases:
      • Client query parameter serialization (limit, offset, columnId, status).
      • Mocked backend with 120 items paginating correctly across multiple pages.
      • useFeatureRequests initial load, incremental appending, and hasMore detection.
      • Overlapping ID deduplication.
      • Pull-to-refresh reset to page 0.
      • In-place item mutation via applyItemChange.
      • Complete dictionary assertions verifying loadingMore is populated across all 14 locales.

Verification

  • npm test: All 71 tests passing cleanly (0 failures).
  • npm run typecheck: Passed with 0 errors (tsc --noEmit).
  • npm run build: ESM, CommonJS, and .d.ts declaration files compile without errors.
  • npm run docs: TypeDoc generation succeeds without warnings.

…oadmap (#5)

- Support `limit`, `offset`, `columnId`, and `status` parameters in `FeedbackClient.fetchFeatureRequests`
- Introduce reusable `useFeatureRequests` pagination hook with deduplication, cancellation, and in-place item mutation
- Implement infinite scroll with `onEndReached` and footer loading indicator in `FeatureRequestsScreen`
- Add query / filter reset and ensure pull-to-refresh restarts at page 0
- Add infinite scrolling and 'Showing N of M' pagination affordance to `RoadmapBoardScreen`
- Add localized copy (`loadingMore`, `loadMore`, `showingCount`) across all 14 supported locales
- Add test coverage for client serialization, backend pagination over 120 items, hook lifecycle, and locale dictionaries
…e state updater, and drop unsupported columnId/status params

- Always clear isLoadingMoreRef/isLoadingMore in loadMore's finally block:
  refresh()/reload()/filter changes abort an in-flight load-more, and the
  previous conditional reset left the guard stuck at true, permanently
  disabling infinite scroll for the lifetime of the component.
- Compute the dedup append count and short-page total clamp from itemsRef
  before calling setState. React invokes functional updaters during the
  next render (and twice under StrictMode), so mutating nextTotal inside
  the setItems updater was never visible to the following setTotal call.
- Remove columnId/status from fetchFeatureRequests and useFeatureRequests:
  GET /api/v1/feature-requests only supports appKey/userToken/limit/offset/
  versionId/q, so the server silently ignored these filters and returned
  unfiltered results. Neither screen passed them; AGENTS.md/README claims
  corrected accordingly.
- Add regression tests for the guard reset and the short-page total clamp.
@lexrus

lexrus commented Sep 5, 2026

Copy link
Copy Markdown
Contributor Author

Code Review

Reviewed the full diff against origin/main (hook, both screens, client, i18n ×14, docs, tests) and cross-checked the pagination contract against the CupThread API backend source. The refactor itself is solid: extracting useFeatureRequests removes real duplication between the two screens, the debounce/abort lifecycle is correct, the empty-column "Load more" affordance on the board is a thoughtful touch, and per-column client-side filtering over a paginated global list is a reasonable trade-off for the current API. CI green; I also ran npm run typecheck, npm test (73 passing), and npm run build locally.

I found three must-fix issues and pushed them in 2a61daf.

Must-fix 1 — isLoadingMoreRef stays stuck after an abort, permanently disabling infinite scroll

loadMore's finally only reset the guard when the request was not aborted:

if (!controller.signal.aborted) {
  isLoadingMoreRef.current = false;
  setIsLoadingMore(false);
}

But refresh(), reload(), and the query/filter effect all abort an in-flight load-more via loadMoreControllerRef.current?.abort(). On that path the reset was skipped, so isLoadingMoreRef.current stayed true forever and every future loadMore() early-returned at the guard (with the footer spinner stuck on). Repro: pull-to-refresh while the next page is loading, then scroll to the end — nothing loads until remount. The fix resets the guard unconditionally; setIsLoadingMore(false) is a safe no-op after unmount.

Must-fix 2 — nextTotal clamp mutated inside the setItems functional updater but read synchronously

setItems((prev) => { ... nextTotal = Math.min(nextTotal, next.length); return next; });
setTotal(nextTotal); // reads the pre-clamp value

React invokes state updaters during the subsequent render, not synchronously at the call site (and StrictMode invokes them twice), so the clamped value was almost never visible to setTotal. The clamp exists precisely to stop hasMore staying true when the server's total drifts from retrievable items (e.g. deletions between page fetches + dedup), which otherwise causes endless empty-page refetches at the end of the list. Same anti-pattern class we fixed in PR #15 (a7c2b1d). The fix computes the dedup count and clamp from itemsRef.current before setState, keeping the updater pure.

Must-fix 3 — columnId/status params are not supported by the API (silent unfiltered results)

GET /api/v1/feature-requests on the backend only reads appKey, userToken, limit, offset, versionId, and qlistPublicFeatureRequests has no column/status predicate in its WHERE clause (verified in SaaS/apps/api/src/index.ts and lib/db.ts; the contract docs list only limit/offset/versionId/q). The params were silently ignored, so useFeatureRequests({ columnId }) would return unfiltered results with a mismatched total — a silent-wrong-data footgun, and repo quality rule #3 requires strict contract consistency. Since neither screen passed these options, I removed them from the client and hook and corrected the AGENTS.md/README claims. They're trivial to re-add in the same PR once the server ships the filters.

Non-blocking suggestions (fine to defer)

  1. RoadmapBoardScreen refetches columns on every column switchloadColumns depends on selectedColumnId, so the effect re-runs (refetch + setIsColumnsLoading(true)) each time the user taps a column. Using setSelectedColumnId(prev => prev ?? visibleCols[0].id) and dropping the dep avoids it.
  2. Unused i18n keysfeatureRequests.loadingMore and roadmap.loadingMore are defined in all 14 locales but never rendered (both footers use common.loadingMore). Worth pruning or wiring up.
  3. Test harness diverges from real React semantics — the hand-rolled ReactCurrentDispatcher harness applies functional updaters synchronously, which is exactly why must-fix 2 passed the suite. It also pins tests to React private internals. Consider react-test-renderer in a follow-up; the implementation fix makes the hook correct regardless of updater timing, and I added regression tests for both the guard reset and the short-page clamp.

With 2a61daf applied: typecheck ✅, 73/73 tests ✅, build ✅. LGTM once CI is green — nothing else blocking.

@lexrus
lexrus merged commit 1efde3f into main Sep 5, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request lists never paginate — silently capped at 50 (list) / 100 (roadmap) items

1 participant