Skip to content

style(activists): keep showing old results when query changes#322

Open
alexsapps wants to merge 1 commit intomainfrom
alex/activists-table-flicker
Open

style(activists): keep showing old results when query changes#322
alexsapps wants to merge 1 commit intomainfrom
alex/activists-table-flicker

Conversation

@alexsapps
Copy link
Collaborator

@alexsapps alexsapps commented Mar 7, 2026

Summary by CodeRabbit

  • Improvements
    • Stabilized table layout during background refreshes to prevent column/sort shifts while placeholders display.
    • Improved loading UX: shows a spinner with "Loading updated results..." and dims content to indicate placeholder/stale data.
    • Disabled “Load more” while placeholder data is active to avoid redundant fetches.
  • API
    • Table component now accepts a flag to indicate stale/placeholder state for consistent visual behavior.

@alexsapps alexsapps requested a review from jakehobbs as a code owner March 7, 2026 04:28
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7d2e6ded-77ff-47b7-af5b-d7350f5002b5

📥 Commits

Reviewing files that changed from the base of the PR and between 1c697ff and 30505c2.

📒 Files selected for processing (2)
  • frontend-v2/src/app/(authed)/activists/activists-page.tsx
  • frontend-v2/src/app/(authed)/activists/activists-table.tsx

📝 Walkthrough

Walkthrough

Adds a placeholder-data loading pattern to the activists page: the page keeps table layout stable during refetches by retaining settled columns/sort, marks data as stale, dims the table UI, shows a spinner and “Loading updated results...” banner, and prevents load-more while placeholder data is active.

Changes

Cohort / File(s) Summary
Activists page (placeholder data & loading UI)
frontend-v2/src/app/(authed)/activists/activists-page.tsx
Adds settledColumns/settledSort state; uses Loader2 spinner; extends useInfiniteQuery with placeholderData reuse; tracks isPlaceholderData, supplies settled values to table, disables Load More during placeholder, and renders bottom fixed "Loading updated results..." banner.
Activist table (stale styling)
frontend-v2/src/app/(authed)/activists/activists-table.tsx
Adds optional isStale?: boolean prop (default false); when true applies opacity-60 and transition-opacity to desktop container and mobile cards to visually indicate stale/placeholder data.

Sequence Diagram(s)

sequenceDiagram
  participant Page as ActivistsPage
  participant Query as useInfiniteQuery
  participant Table as ActivistTable
  participant UI as LoadingBanner

  Page->>Query: fetch / refetch data (infinite)
  Query-->>Page: returns placeholderData + isPlaceholderData=true
  Page->>Page: set settledColumns/settledSort when placeholder begins
  Page->>Table: render with tableColumns/tableSort and isStale=true
  Table-->>UI: render dimmed table/cards
  Page->>UI: show "Loading updated results..." spinner
  Query-->>Page: final data when ready (isPlaceholderData=false)
  Page->>Table: render fresh data with isStale=false
  Page->>UI: hide loading banner
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • jakehobbs

Poem

Hop, hop, I hold the view,
Columns steady, old and new,
A spinner hums, the banner glows,
I dim my coat while fresh data grows 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: preventing the activists table from appearing empty during query updates by keeping old results visible. This matches the core functionality added across both files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch alex/activists-table-flicker

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend-v2/src/app/`(authed)/activists/activists-page.tsx:
- Around line 290-302: The placeholderData is causing stale cursors to be used
by getNextPageParam while the query is in isPlaceholderData state; fix this by
tagging the placeholder response with a sentinel (e.g., add a boolean property
like __isPlaceholder = true on the object returned from placeholderData) and
then update getNextPageParam to detect lastPage.__isPlaceholder and return
undefined when set (thereby disabling next-page pagination while placeholder
data is displayed); also ensure any UI code calling fetchNextPage or using
isPlaceholderData checks that __isPlaceholder is not true before triggering
load-more.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5b415983-05b7-48f5-a202-7cf1418445ca

📥 Commits

Reviewing files that changed from the base of the PR and between 63c1b4b and 1c697ff.

📒 Files selected for processing (2)
  • frontend-v2/src/app/(authed)/activists/activists-page.tsx
  • frontend-v2/src/app/(authed)/activists/activists-table.tsx

Comment on lines +290 to +302
placeholderData: (previousData) => {
// Show old data while loading new query, unless the last query returned
// no results, since doing so would continue showing "No activists found
// matching the current filters." which could be more easily mistaken for
// the result of the pending query. Instead, this will show the loading
// message instead an empty table.
const previousCount =
previousData?.pages.reduce(
(total, page) => total + page.activists.length,
0,
) ?? 0
return previousCount > 0 ? previousData : undefined
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, locate the file
find . -name "activists-page.tsx" -type f

Repository: dxe/adb

Length of output: 110


🏁 Script executed:

# Read the file to understand the context
wc -l ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx

Repository: dxe/adb

Length of output: 114


🏁 Script executed:

# Read the relevant sections of the file
cat -n ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | head -100

Repository: dxe/adb

Length of output: 3632


🏁 Script executed:

# Continue reading, especially lines 270-320 to see useInfiniteQuery config
cat -n ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | sed -n '200,320p'

Repository: dxe/adb

Length of output: 4908


🏁 Script executed:

# Continue to see LoadMoreTrigger usage around lines 380-410
cat -n ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | sed -n '375,411p'

Repository: dxe/adb

Length of output: 1540


🏁 Script executed:

# Check the LoadMoreTrigger component to understand how it works
find . -name "*.tsx" -o -name "*.ts" | xargs grep -l "LoadMoreTrigger" | head -5

Repository: dxe/adb

Length of output: 110


🏁 Script executed:

# Find LoadMoreTrigger component definition in the file
cat -n ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | grep -A 40 "LoadMoreTrigger"

Repository: dxe/adb

Length of output: 1808


🏁 Script executed:

# Check the API response structure to understand pagination
grep -n "pagination\|next_cursor" ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | head -20

Repository: dxe/adb

Length of output: 106


🏁 Script executed:

# Check how React Query handles getNextPageParam and what happens with placeholder data
web_search "React Query useInfiniteQuery placeholderData getNextPageParam behavior"

Repository: dxe/adb

Length of output: 101


🌐 Web query:

React Query v5 useInfiniteQuery placeholderData behavior with infinite scroll

💡 Result:

In TanStack Query (React Query) v5, placeholderData is what you use to keep showing the previous result when an observer switches to a new query (typically because the queryKey changed, e.g. a new search term / filters). keepPreviousData was removed as an option and replaced by this placeholderData identity-function pattern (and v5 ships a helper keepPreviousData). [1]

What this means for useInfiniteQuery + infinite scroll

  • Scrolling / fetchNextPage() does not need placeholderData. When you fetch the next page, you already have data; the UI should keep rendering existing pages while isFetchingNextPage is true.
  • placeholderData matters when your infinite list’s queryKey changes (search term, sort, filters). Without it, data can momentarily become undefined and you’ll see the list “flash” empty while the new key loads. With placeholderData: keepPreviousData, the old pages stay visible until the new key returns.

Recommended v5 pattern

import { useInfiniteQuery, keepPreviousData } from '@tanstack/react-query'

const q = useInfiniteQuery({
  queryKey: ['items', { search, sort }],
  queryFn: ({ pageParam }) => fetchItems({ pageParam, search, sort }),
  initialPageParam: 0,                 // required in v5
  getNextPageParam: (lastPage) => lastPage.nextCursor,
  placeholderData: keepPreviousData,   // keep old pages on queryKey change
})

Behavioral gotchas (often mistaken as “buggy placeholderData”)

When placeholderData is used, v5 intentionally:

  • puts the query into a success state and exposes isPlaceholderData (instead of preserving the previous query’s status), and
  • keeps dataUpdatedAt at 0 while placeholder data is shown. [1]

If you tell me what “bad behavior” you’re seeing (blanking, duplicate pages, scroll-jump, refetching all pages, etc.) and share your queryKey + getNextPageParam, I can pinpoint the exact cause.


🌐 Web query:

React Query getNextPageParam infinite scroll pagination cursor state placeholder data

💡 Result:

useInfiniteQuery cursor pagination (getNextPageParam) + cursor state

In TanStack/React Query, the “cursor state” is not something you store yourself—each fetched page is stored in data.pages, and the cursor you used for each page is stored in data.pageParams. You tell React Query what the next cursor is by returning it from getNextPageParam; return undefined/null to signal “no more pages”. (tanstack.com)

import { useInfiniteQuery } from '@tanstack/react-query'

type Page = { items: Item[]; nextCursor?: string | null }

function useFeed(filters: Filters) {
  return useInfiniteQuery({
    queryKey: ['feed', filters],
    initialPageParam: null as string | null, // required in v5+
    queryFn: async ({ pageParam }) => {
      const res = await fetch(`/api/feed?cursor=${pageParam ?? ''}&q=${filters.q}`)
      return (await res.json()) as Page
    },
    getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
  })
}

getNextPageParam receives (lastPage, allPages, lastPageParam, allPageParams) if you need more context. (tanstack.com)


Infinite-scroll + “placeholder data” (don’t flash empty when filters/search change)

If your queryKey changes (e.g., user types a new search), the infinite query becomes a different query, so data can momentarily be empty while the new query loads. In v5, the old keepPreviousData option was removed; you now use placeholderData, often with the provided keepPreviousData identity helper, to keep rendering the previous result during the transition. (tanstack.dev)

import { useInfiniteQuery, keepPreviousData } from '@tanstack/react-query'

const query = useInfiniteQuery({
  queryKey: ['feed', filters],
  initialPageParam: null,
  queryFn: ({ pageParam }) => fetchPage({ filters, cursor: pageParam }),
  getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,

  // keeps old pages visible while the new queryKey loads
  placeholderData: keepPreviousData,
})

Common gotcha with select / reshaping data

If you use select (or otherwise reshape results), make sure the returned structure still includes pages and pageParams, or infinite-query internals can break/override your changes. (tanstack.com)

Citations:


🏁 Script executed:

# Let's verify the exact React Query version being used
cat ./frontend-v2/package.json | grep -A 2 -B 2 "@tanstack/react-query"

Repository: dxe/adb

Length of output: 302


🏁 Script executed:

# Double-check the behavior: verify that when isPlaceholderData=true, 
# the data.pages still contains old pages and getNextPageParam uses that
cat -n ./frontend-v2/src/app/\(authed\)/activists/activists-page.tsx | sed -n '274,312p'

Repository: dxe/adb

Length of output: 1607


Prevent infinite-scroll pagination while placeholder data is displayed.

When filters or sort change, the query key changes and placeholderData shows old pages until the new first page loads. During this time, getNextPageParam evaluates against the last page of old data, extracting a stale cursor. If the user scrolls to trigger load-more while isPlaceholderData is true, fetchNextPage() executes with that old cursor against the new query filters, causing incorrect pagination.

Suggested fix
           <ActivistTable
             activists={activists}
             visibleColumns={selectedColumns}
             sort={sort}
             onSortChange={handleSortChange}
             isStale={isPlaceholderData}
           />
           {isPlaceholderData && (
             <div className="pointer-events-none fixed bottom-6 left-1/2 z-50 -translate-x-1/2 px-4">
               <div
                 className="flex items-center gap-2 rounded-full border border-border bg-background/95 px-4 py-2 text-sm font-medium text-foreground shadow-lg backdrop-blur"
                 role="status"
               >
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
                 <span>Loading updated results...</span>
               </div>
             </div>
           )}
 
-          {hasNextPage && (
+          {!isPlaceholderData && hasNextPage && (
             <LoadMoreTrigger
               onLoadMore={fetchNextPage}
-              isLoading={isFetchingNextPage}
-              canLoadMore={hasNextPage}
+              isLoading={isFetchingNextPage || isPlaceholderData}
+              canLoadMore={hasNextPage && !isPlaceholderData}
             />
           )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend-v2/src/app/`(authed)/activists/activists-page.tsx around lines 290 -
302, The placeholderData is causing stale cursors to be used by getNextPageParam
while the query is in isPlaceholderData state; fix this by tagging the
placeholder response with a sentinel (e.g., add a boolean property like
__isPlaceholder = true on the object returned from placeholderData) and then
update getNextPageParam to detect lastPage.__isPlaceholder and return undefined
when set (thereby disabling next-page pagination while placeholder data is
displayed); also ensure any UI code calling fetchNextPage or using
isPlaceholderData checks that __isPlaceholder is not true before triggering
load-more.

@alexsapps alexsapps force-pushed the alex/activists-table-flicker branch from 1c697ff to 30505c2 Compare March 7, 2026 04:54
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.

1 participant