style(activists): keep showing old results when query changes#322
style(activists): keep showing old results when query changes#322
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (2)
frontend-v2/src/app/(authed)/activists/activists-page.tsxfrontend-v2/src/app/(authed)/activists/activists-table.tsx
| 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 | ||
| }, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the file
find . -name "activists-page.tsx" -type fRepository: 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.tsxRepository: 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 -100Repository: 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 -5Repository: 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 -20Repository: 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 needplaceholderData. When you fetch the next page, you already have data; the UI should keep rendering existing pages whileisFetchingNextPageis true. placeholderDatamatters when your infinite list’squeryKeychanges (search term, sort, filters). Without it,datacan momentarily becomeundefinedand you’ll see the list “flash” empty while the new key loads. WithplaceholderData: 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
dataUpdatedAtat 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:
- 1: https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery/?utm_source=openai
- 2: https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery/?utm_source=openai
- 3: https://tanstack.dev/query/latest/docs/framework/vue/guides/migrating-to-v5?utm_source=openai
- 4: https://tanstack.com/query/v4/docs/framework/react/guides/infinite-queries?utm_source=openai
🏁 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.
1c697ff to
30505c2
Compare
Summary by CodeRabbit