Skip to content

Commit a216e2d

Browse files
committed
feat: enhance useArrayVirtualizer to include listContextParams and update ListPage to utilize new scroll state management
1 parent aa94f02 commit a216e2d

File tree

3 files changed

+81
-33
lines changed

3 files changed

+81
-33
lines changed

apps/zbugs/src/array-test-app.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,12 @@ function ArrayTestAppContent() {
176176
permalinkNotFound,
177177
estimatedTotal,
178178
total,
179-
} = useArrayVirtualizer<RowData, IssueRowSort>({
179+
} = useArrayVirtualizer<RowData, IssueRowSort, string>({
180180
getPageQuery,
181181
getSingleQuery,
182182
toStartRow,
183183
permalinkID,
184+
listContextParams: 'test-list',
184185
scrollState,
185186
onScrollStateChange,
186187

apps/zbugs/src/hooks/use-array-virtualizer.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ type TanstackUseVirtualizerOptions<
2424
*
2525
* @typeParam TRow - The type of row data returned from queries
2626
* @typeParam TSort - The type of data needed to anchor pagination (typically a subset of T)
27+
* @typeParam TListContextParams - The type of parameters that define the list's query context
2728
* @typeParam TScrollElement - The type of the scrollable container element
2829
* @typeParam TItemElement - The type of the individual item elements
2930
*/
3031
export interface UseArrayVirtualizerOptions<
3132
TRow,
3233
TSort,
34+
TListContextParams,
3335
TScrollElement extends Element = HTMLElement,
3436
TItemElement extends Element = Element,
3537
> extends Omit<
@@ -56,6 +58,8 @@ export interface UseArrayVirtualizerOptions<
5658
toStartRow: (row: TRow) => TSort;
5759
/** Optional ID to highlight/scroll to a specific row (permalink functionality) */
5860
permalinkID?: string | undefined;
61+
/** Parameters that define the list's query context (filters, sort order, etc.) */
62+
listContextParams: TListContextParams;
5963
/**
6064
* Controlled scroll state. When the consumer provides a new value that
6165
* differs from what the hook last reported via `onScrollStateChange`, the
@@ -212,6 +216,7 @@ const POSITIONING_SETTLE_DELAY_MS = 50;
212216
export function useArrayVirtualizer<
213217
T,
214218
TSort,
219+
TListContextParams,
215220
TScrollElement extends Element = HTMLElement,
216221
TItemElement extends Element = Element,
217222
>({
@@ -221,6 +226,7 @@ export function useArrayVirtualizer<
221226
getSingleQuery,
222227
toStartRow,
223228
permalinkID,
229+
listContextParams,
224230
scrollState: externalScrollState,
225231
onScrollStateChange,
226232
debug = false,
@@ -229,6 +235,7 @@ export function useArrayVirtualizer<
229235
}: UseArrayVirtualizerOptions<
230236
T,
231237
TSort,
238+
TListContextParams,
232239
TScrollElement,
233240
TItemElement
234241
>): UseArrayVirtualizerReturn<T, TScrollElement, TItemElement> {
@@ -253,6 +260,22 @@ export function useArrayVirtualizer<
253260
const [hasReachedStart, setHasReachedStart] = useState(false);
254261
const [hasReachedEnd, setHasReachedEnd] = useState(false);
255262

263+
// Track the last listContextParams to detect context changes
264+
const prevListContextParamsRef = useRef(listContextParams);
265+
const isListContextCurrent =
266+
prevListContextParamsRef.current === listContextParams;
267+
268+
// Reset pagination state when list context changes
269+
useEffect(() => {
270+
if (!isListContextCurrent) {
271+
prevListContextParamsRef.current = listContextParams;
272+
setMinIndexSeen(null);
273+
setMaxIndexSeen(null);
274+
setHasReachedStart(false);
275+
setHasReachedEnd(false);
276+
}
277+
}, [isListContextCurrent, listContextParams]);
278+
256279
const scrollInternalRef = useRef({
257280
pendingScroll: null as number | null,
258281
pendingScrollIsRelative: false,

apps/zbugs/src/pages/list/list-page.tsx

Lines changed: 56 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useQuery, useZero, useZeroVirtualizer} from '@rocicorp/zero/react';
1+
import {useQuery, useZero} from '@rocicorp/zero/react';
22
import classNames from 'classnames';
33
import Cookies from 'js-cookie';
44
import React, {
@@ -13,6 +13,7 @@ import React, {
1313
import {toast} from 'react-toastify';
1414
import {useDebouncedCallback} from 'use-debounce';
1515
import {useParams, useSearch} from 'wouter';
16+
import {useHistoryState} from 'wouter/use-browser-location';
1617
import {must} from '../../../../../packages/shared/src/must.ts';
1718
import {
1819
queries,
@@ -26,16 +27,18 @@ import {IssueLink} from '../../components/issue-link.tsx';
2627
import {Link} from '../../components/link.tsx';
2728
import {OnboardingModal} from '../../components/onboarding-modal.tsx';
2829
import {RelativeTime} from '../../components/relative-time.tsx';
30+
import {
31+
useArrayVirtualizer,
32+
type ScrollRestorationState,
33+
} from '../../hooks/use-array-virtualizer.ts';
2934
import {useClickOutside} from '../../hooks/use-click-outside.ts';
3035
import {useElementSize} from '../../hooks/use-element-size.ts';
3136
import {useHash} from '../../hooks/use-hash.ts';
3237
import {useKeypress} from '../../hooks/use-keypress.ts';
3338
import {useLogin} from '../../hooks/use-login.tsx';
34-
import {useWouterPermalinkState} from '../../hooks/use-wouter-permalink-state.ts';
3539
import {appendParam, navigate, removeParam, setParam} from '../../navigate.ts';
3640
import {recordPageLoad} from '../../page-load-stats.ts';
3741
import {mark} from '../../perf-log.ts';
38-
import {CACHE_NAV, CACHE_NONE} from '../../query-cache-policy.ts';
3942
import {isGigabugs, links, useListContext} from '../../routes.tsx';
4043
import {preload} from '../../zero-preload.ts';
4144
import {getIDFromString} from '../issue/get-id.tsx';
@@ -109,7 +112,6 @@ export function ListPage({onReady}: {onReady: () => void}) {
109112
labels,
110113
open,
111114
textFilter,
112-
permalinkID,
113115
}) as const,
114116
[
115117
projectName,
@@ -120,7 +122,6 @@ export function ListPage({onReady}: {onReady: () => void}) {
120122
open,
121123
textFilter,
122124
labels,
123-
permalinkID,
124125
],
125126
);
126127

@@ -162,30 +163,31 @@ export function ListPage({onReady}: {onReady: () => void}) {
162163
navigate(`#issue-${id}`);
163164
};
164165

165-
const [permalinkState, setPermalinkState] =
166-
useWouterPermalinkState<IssueRowSort>();
166+
const scrollStateFromHistory =
167+
useHistoryState<ScrollRestorationState | null>();
167168

168-
const {
169-
virtualizer,
170-
rowAt,
171-
complete,
172-
rowsEmpty,
173-
permalinkNotFound,
174-
estimatedTotal,
175-
total,
176-
} = useZeroVirtualizer({
177-
estimateSize: () => ITEM_SIZE,
178-
getScrollElement: () => listRef.current,
179-
getRowKey: row => row.id,
169+
const scrollState = useMemo(
170+
() => scrollStateFromHistory,
171+
[
172+
scrollStateFromHistory?.scrollAnchorID,
173+
scrollStateFromHistory?.index,
174+
scrollStateFromHistory?.scrollOffset,
175+
],
176+
);
180177

181-
listContextParams,
182-
permalinkID,
178+
const handleScrollStateChange = useCallback(
179+
(state: ScrollRestorationState) => {
180+
window.history.replaceState(state, '', window.location.href);
181+
},
182+
[],
183+
);
183184

184-
getPageQuery: (
185-
limit: number,
186-
start: IssueRowSort | null,
187-
dir: 'forward' | 'backward',
188-
) =>
185+
const estimateRowSize = useCallback(() => ITEM_SIZE, []);
186+
187+
const getScrollElement = useCallback(() => listRef.current, []);
188+
189+
const getPageQuery = useCallback(
190+
(limit: number, start: IssueRowSort | null, dir: 'forward' | 'backward') =>
189191
queries.issueListV2({
190192
listContext: listContextParams,
191193
userID: z.userID,
@@ -194,8 +196,11 @@ export function ListPage({onReady}: {onReady: () => void}) {
194196
dir,
195197
inclusive: start === null,
196198
}),
199+
[listContextParams, z.userID],
200+
);
197201

198-
getSingleQuery: (id: string) => {
202+
const getSingleQuery = useCallback(
203+
(id: string) => {
199204
// Allow short ID too.
200205
const {idField, idValue} = getIDFromString(id);
201206
return queries.listIssueByID({
@@ -204,17 +209,36 @@ export function ListPage({onReady}: {onReady: () => void}) {
204209
listContext: listContextParams,
205210
});
206211
},
212+
[listContextParams],
213+
);
207214

208-
toStartRow: row => ({
215+
const toStartRow = useCallback(
216+
(row: {id: string; modified: number; created: number}) => ({
209217
id: row.id,
210218
modified: row.modified,
211219
created: row.created,
212220
}),
221+
[],
222+
);
213223

214-
options: textFilterQuery === textFilter ? CACHE_NAV : CACHE_NONE,
215-
216-
permalinkState,
217-
onPermalinkStateChange: setPermalinkState,
224+
const {
225+
virtualizer,
226+
rowAt,
227+
complete,
228+
rowsEmpty,
229+
permalinkNotFound,
230+
estimatedTotal,
231+
total,
232+
} = useArrayVirtualizer({
233+
estimateRowSize,
234+
getScrollElement,
235+
permalinkID: permalinkID ?? undefined,
236+
listContextParams,
237+
getPageQuery,
238+
getSingleQuery,
239+
toStartRow,
240+
scrollState: scrollState ?? undefined,
241+
onScrollStateChange: handleScrollStateChange,
218242
});
219243

220244
useEffect(() => {

0 commit comments

Comments
 (0)