From 2f06c60c8d8e93017c23b349edc4d7d5c1f78981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 15 May 2026 17:41:52 +0800 Subject: [PATCH 1/4] refactor grouped data hooks --- src/List.tsx | 63 +++++++++------ src/hooks/index.ts | 6 +- src/hooks/useFlattenData.ts | 53 +++++++++++++ src/hooks/useFlattenRows.ts | 52 ------------ src/hooks/useGroupData.ts | 44 +++++++++++ src/hooks/useGroupSegments.ts | 55 ------------- src/hooks/useStickyGroupHeader.tsx | 2 +- src/index.ts | 2 +- src/interface.ts | 39 --------- src/util/index.ts | 7 -- tests/hooks.test.tsx | 123 +++++++++++++++++++++-------- tests/listy.behavior.test.tsx | 13 --- 12 files changed, 233 insertions(+), 226 deletions(-) create mode 100644 src/hooks/useFlattenData.ts delete mode 100644 src/hooks/useFlattenRows.ts create mode 100644 src/hooks/useGroupData.ts delete mode 100644 src/hooks/useGroupSegments.ts delete mode 100644 src/interface.ts delete mode 100644 src/util/index.ts diff --git a/src/List.tsx b/src/List.tsx index bf96e8f..d4e367b 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -1,15 +1,46 @@ import * as React from 'react'; import VirtualList, { type ListRef } from 'rc-virtual-list'; -import type { ListyProps, ListyRef } from './interface'; +import type { ScrollTo } from 'rc-virtual-list/lib/List'; import { useImperativeHandle, forwardRef } from 'react'; -import useGroupSegments from './hooks/useGroupSegments'; -import useFlattenRows from './hooks/useFlattenRows'; -import type { Row } from './hooks/useFlattenRows'; +import useGroupData from './hooks/useGroupData'; +import type { Group } from './hooks/useGroupData'; +import useFlattenData from './hooks/useFlattenData'; +import type { Row } from './hooks/useFlattenData'; import useStickyGroupHeader from './hooks/useStickyGroupHeader'; -import { isGroupScrollConfig } from './util'; import clsx from 'clsx'; import { useEvent } from '@rc-component/util'; +type RowKey = keyof T | ((item: T) => React.Key); + +export type ScrollAlign = 'top' | 'bottom' | 'auto'; + +export interface GroupScrollToConfig { + groupKey: string; + align?: ScrollAlign; + offset?: number; +} + +export type ListyScrollToConfig = + | Parameters[0] + | GroupScrollToConfig; + +export interface ListyRef { + scrollTo: (config?: ListyScrollToConfig) => void; +} + +export interface ListyProps { + items?: T[]; + sticky?: boolean; + itemHeight?: number; + height?: number; + group?: Group; + virtual?: boolean; + prefixCls?: string; + rowKey: RowKey; + onScroll?: React.UIEventHandler; + itemRender: (item: T, index: number) => React.ReactNode; +} + function Listy( props: ListyProps, ref: React.Ref, @@ -38,7 +69,7 @@ function Listy( // ========================== Imperative API ========================== useImperativeHandle(ref, () => ({ scrollTo: (config) => { - if (isGroupScrollConfig(config)) { + if (config && typeof config === 'object' && 'groupKey' in config) { const { groupKey, align, offset } = config; listRef.current?.scrollTo({ key: groupKey, @@ -47,12 +78,12 @@ function Listy( }); return; } - listRef.current?.scrollTo(config); + listRef.current?.scrollTo(config as Parameters[0]); }, })); // ============================= Grouping ============================= - const groupSegments = useGroupSegments(data, group); + const groupData = useGroupData(data, group); // ============================= Row Keys ============================= const getKey = useEvent((row: Row): React.Key => { @@ -67,24 +98,12 @@ function Listy( }); // ============================= Flat Rows ============================= - const { rows, headerRows, groupKeyToSeg } = useFlattenRows( + const { rows, headerRows, groupKeyToItems } = useFlattenData( data, + groupData, group, - groupSegments, ); - // ============================ Group Items ============================ - const groupKeyToItems = React.useMemo(() => { - const map = new Map(); - if (!group) { - return map; - } - groupKeyToSeg.forEach(({ startIndex, endIndex }, key) => { - map.set(key, data.slice(startIndex, endIndex + 1)); - }); - return map; - }, [group, groupKeyToSeg, data]); - // =========================== Sticky Header =========================== const extraRender = useStickyGroupHeader({ enabled: !!(sticky && group), diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 67314fb..9315775 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,5 +1,5 @@ -import useGroupSegments from './useGroupSegments'; +import useGroupData from './useGroupData'; +import useFlattenData from './useFlattenData'; import useStickyGroupHeader from './useStickyGroupHeader'; -import useFlattenRows from './useFlattenRows'; -export { useGroupSegments, useStickyGroupHeader, useFlattenRows }; +export { useGroupData, useFlattenData, useStickyGroupHeader }; diff --git a/src/hooks/useFlattenData.ts b/src/hooks/useFlattenData.ts new file mode 100644 index 0000000..eb509bd --- /dev/null +++ b/src/hooks/useFlattenData.ts @@ -0,0 +1,53 @@ +import * as React from 'react'; +import type { Group, GroupDataItem } from './useGroupData'; + +export type Row = + | { type: 'header'; groupKey: K } + | { type: 'item'; item: T; index: number }; + +export interface FlattenDataResult { + rows: Row[]; + headerRows: { groupKey: K; rowIndex: number }[]; + groupKeyToItems: Map; +} + +/** + * Flatten grouped data into header and item rows. + * When grouping is enabled, items follow the insertion order of the group map + * while preserving their original indexes. + */ +export default function useFlattenData( + data: T[], + groupData: Map[]>, + group?: Group, +): FlattenDataResult { + return React.useMemo(() => { + const flatRows: Row[] = []; + const headerRows: { groupKey: K; rowIndex: number }[] = []; + const groupKeyToItems = new Map(); + + if (!group) { + data.forEach((item, index) => { + flatRows.push({ type: 'item', item, index }); + }); + + return { rows: flatRows, headerRows, groupKeyToItems }; + } + + groupData.forEach((groupItems, groupKey) => { + groupKeyToItems.set( + groupKey, + groupItems.map(({ item }) => item), + ); + + headerRows.push({ groupKey, rowIndex: flatRows.length }); + flatRows.push({ type: 'header', groupKey }); + + groupItems.forEach(({ item, index }) => { + flatRows.push({ type: 'item', item, index }); + }); + }); + + return { rows: flatRows, headerRows, groupKeyToItems }; + }, [data, group, groupData]); +} diff --git a/src/hooks/useFlattenRows.ts b/src/hooks/useFlattenRows.ts deleted file mode 100644 index ea1486c..0000000 --- a/src/hooks/useFlattenRows.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as React from 'react'; -import type { Group } from '../interface'; -import type { GroupSegment } from './useGroupSegments'; - -export type Row = - | { type: 'header'; groupKey: K } - | { type: 'item'; item: T; index: number }; - -export interface FlattenRowsResult { - rows: Row[]; - headerRows: { groupKey: K; rowIndex: number }[]; - groupKeyToSeg: Map; -} - -export default function useFlattenRows( - items: T[], - group: Group | undefined, - segments: GroupSegment[], -): FlattenRowsResult { - return React.useMemo(() => { - const flatRows: Row[] = []; - const headerRows: { groupKey: K; rowIndex: number }[] = []; - const groupKeyToSeg = new Map< - K, - { startIndex: number; endIndex: number } - >(); - - if (!group || !segments.length) { - for (let i = 0; i < items.length; i += 1) { - flatRows.push({ type: 'item', item: items[i], index: i }); - } - return { rows: flatRows, headerRows, groupKeyToSeg }; - } - - for (let s = 0; s < segments.length; s += 1) { - const seg = segments[s]; - groupKeyToSeg.set(seg.key, { - startIndex: seg.startIndex, - endIndex: seg.endIndex, - }); - - headerRows.push({ groupKey: seg.key, rowIndex: flatRows.length }); - flatRows.push({ type: 'header', groupKey: seg.key }); - - for (let i = seg.startIndex; i <= seg.endIndex; i += 1) { - flatRows.push({ type: 'item', item: items[i], index: i }); - } - } - - return { rows: flatRows, headerRows, groupKeyToSeg }; - }, [items, group, segments]); -} diff --git a/src/hooks/useGroupData.ts b/src/hooks/useGroupData.ts new file mode 100644 index 0000000..f06113b --- /dev/null +++ b/src/hooks/useGroupData.ts @@ -0,0 +1,44 @@ +import * as React from 'react'; + +export interface Group { + key: (item: T) => K; + title: (groupKey: K, items: T[]) => React.ReactNode; +} + +export interface GroupDataItem { + item: T; + index: number; +} + +/** + * Build a lookup map from group key to all matching data items and their + * original indexes. + * This groups by key across the full data set and does not require items with + * the same key to be contiguous. + */ +export default function useGroupData( + data: T[], + group?: Group, +): Map[]> { + return React.useMemo(() => { + const map = new Map[]>(); + + if (!group) { + return map; + } + + data.forEach((item, index) => { + const groupKey = group.key(item); + const groupItems = map.get(groupKey); + const groupDataItem = { item, index }; + + if (groupItems) { + groupItems.push(groupDataItem); + } else { + map.set(groupKey, [groupDataItem]); + } + }); + + return map; + }, [data, group]); +} diff --git a/src/hooks/useGroupSegments.ts b/src/hooks/useGroupSegments.ts deleted file mode 100644 index f2d6283..0000000 --- a/src/hooks/useGroupSegments.ts +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import type { Group } from '../interface'; - -export interface GroupSegment { - key: K; - startIndex: number; - endIndex: number; -} - -/** - * segments representing consecutive runs of items that share the same group key. - */ -export default function useGroupSegments( - items: T[], - group?: Group, -): GroupSegment[] { - return React.useMemo(() => { - if (!group || !items?.length) { - return []; - } - - const segments: GroupSegment[] = []; - let currentKey: K | null = null; - let currentStart = -1; - - const getGroupKey = (item: T): K => - typeof group.key === 'function' ? group.key(item) : group.key; - - for (let i = 0; i < items.length; i += 1) { - const gk = getGroupKey(items[i]); - if (currentKey === null) { - currentKey = gk; - currentStart = i; - } else if (gk !== currentKey) { - segments.push({ - key: currentKey, - startIndex: currentStart, - endIndex: i - 1, - }); - currentKey = gk; - currentStart = i; - } - } - - if (currentKey !== null) { - segments.push({ - key: currentKey, - startIndex: currentStart, - endIndex: items.length - 1, - }); - } - - return segments; - }, [items, group]); -} diff --git a/src/hooks/useStickyGroupHeader.tsx b/src/hooks/useStickyGroupHeader.tsx index f65ee6e..d0adfc8 100644 --- a/src/hooks/useStickyGroupHeader.tsx +++ b/src/hooks/useStickyGroupHeader.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Portal from '@rc-component/portal'; import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; -import type { Group } from '../interface'; +import type { Group } from './useGroupData'; export interface StickyHeaderParams { enabled: boolean; diff --git a/src/index.ts b/src/index.ts index d6d300d..69fef22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import Listy from './List'; -export type { ListyRef, ListyProps } from './interface'; +export type { ListyRef, ListyProps } from './List'; export default Listy; diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index aff5c63..0000000 --- a/src/interface.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type * as React from 'react'; -import type { ScrollTo } from 'rc-virtual-list/lib/List'; -import type { GetKey } from 'rc-virtual-list/lib/interface'; - -export type ScrollAlign = 'top' | 'bottom' | 'auto'; - -export type ListyScrollToConfig = - | Parameters[0] - | { - groupKey: string; - align?: ScrollAlign; - offset?: number; - }; - -export interface ListyRef { - scrollTo: (config?: ListyScrollToConfig) => void; -} - -type RowKey = keyof T | ((item: T) => React.Key); - -export interface Group { - key: ((item: T) => K) | K; - title: (groupKey: K, items: T[]) => React.ReactNode; -} - -export interface ListyProps { - items?: T[]; - sticky?: boolean; - itemHeight?: number; - height?: number; - group?: Group; - virtual?: boolean; - prefixCls?: string; - rowKey: RowKey; - onScroll?: React.UIEventHandler; - itemRender: (item: T, index: number) => React.ReactNode; -} - -export type { GetKey }; diff --git a/src/util/index.ts b/src/util/index.ts deleted file mode 100644 index e1e698d..0000000 --- a/src/util/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { ListyScrollToConfig, ScrollAlign } from '../interface'; - -export function isGroupScrollConfig( - config: ListyScrollToConfig, -): config is { groupKey: string; align?: ScrollAlign; offset?: number } { - return !!config && typeof config === 'object' && 'groupKey' in config; -} diff --git a/tests/hooks.test.tsx b/tests/hooks.test.tsx index 7d1bc2a..58a50fa 100644 --- a/tests/hooks.test.tsx +++ b/tests/hooks.test.tsx @@ -3,7 +3,11 @@ import { render, renderHook } from '@testing-library/react'; import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; -import { useGroupSegments, useStickyGroupHeader } from '../src/hooks'; +import { + useFlattenData, + useGroupData, + useStickyGroupHeader, +} from '../src/hooks'; import type { StickyHeaderParams } from '../src/hooks/useStickyGroupHeader'; const PREFIX_CLS = 'rc-listy'; @@ -63,71 +67,124 @@ const createListRef = ( } as React.RefObject; }; -describe('useGroupSegments', () => { - it('creates segments for contiguous group keys', () => { +describe('useGroupData', () => { + it('groups items by key across the full data set', () => { const items: GroupedItem[] = [ { id: 0, group: 'A' }, { id: 1, group: 'A' }, { id: 2, group: 'B' }, { id: 3, group: 'B' }, - { id: 4, group: 'C' }, + { id: 4, group: 'A' }, ]; const { result } = renderHook(() => - useGroupSegments(items, { + useGroupData(items, { key: (item) => item.group, title: () => null, }), ); - expect(result.current).toEqual([ - { key: 'A', startIndex: 0, endIndex: 1 }, - { key: 'B', startIndex: 2, endIndex: 3 }, - { key: 'C', startIndex: 4, endIndex: 4 }, - ]); + expect(result.current).toEqual( + new Map([ + [ + 'A', + [ + { item: items[0], index: 0 }, + { item: items[1], index: 1 }, + { item: items[4], index: 4 }, + ], + ], + [ + 'B', + [ + { item: items[2], index: 2 }, + { item: items[3], index: 3 }, + ], + ], + ]), + ); }); - it('supports static group keys and empty states', () => { - const staticGroup = { key: 'static', title: () => null }; + it('supports empty states', () => { + const staticGroup = { key: () => 'static', title: () => null }; const { result: staticResult } = renderHook(() => - useGroupSegments([{ id: 1, group: 'unused' }], staticGroup), + useGroupData([{ id: 1, group: 'unused' }], staticGroup), ); - expect(staticResult.current).toEqual([ - { key: 'static', startIndex: 0, endIndex: 0 }, - ]); + expect(staticResult.current).toEqual( + new Map([['static', [{ item: { id: 1, group: 'unused' }, index: 0 }]]]), + ); const { result: noGroup } = renderHook(() => - useGroupSegments([{ id: 1, group: 'A' }], undefined), + useGroupData([{ id: 1, group: 'A' }], undefined), ); - expect(noGroup.current).toEqual([]); + expect(noGroup.current).toEqual(new Map()); const { result: noItems } = renderHook(() => - useGroupSegments([], { + useGroupData([], { key: (item) => item.group, title: () => null, }), ); - expect(noItems.current).toEqual([]); + expect(noItems.current).toEqual(new Map()); }); +}); - it('handles inconsistent length lookups', () => { - const trickyItems: any = { 0: { id: 9, group: 'Z' }, __calls: 0 }; - Object.defineProperty(trickyItems, 'length', { - get() { - this.__calls += 1; - return this.__calls === 1 ? 1 : 0; - }, +describe('useFlattenData', () => { + it('flattens grouped data into header and item rows', () => { + const items: GroupedItem[] = [ + { id: 0, group: 'A' }, + { id: 1, group: 'B' }, + { id: 2, group: 'A' }, + ]; + const group = { + key: (item: GroupedItem) => item.group, + title: () => null, + }; + + const { result } = renderHook(() => { + const groupData = useGroupData(items, group); + return useFlattenData(items, groupData, group); }); - const { result } = renderHook(() => - useGroupSegments(trickyItems as GroupedItem[], { - key: (item) => item?.group ?? 'fallback', - title: () => null, - }), + expect(result.current.rows).toEqual([ + { type: 'header', groupKey: 'A' }, + { type: 'item', item: items[0], index: 0 }, + { type: 'item', item: items[2], index: 2 }, + { type: 'header', groupKey: 'B' }, + { type: 'item', item: items[1], index: 1 }, + ]); + expect(result.current.headerRows).toEqual([ + { groupKey: 'A', rowIndex: 0 }, + { groupKey: 'B', rowIndex: 3 }, + ]); + expect(result.current.groupKeyToItems).toEqual( + new Map([ + ['A', [items[0], items[2]]], + ['B', [items[1]]], + ]), ); + }); + + it('flattens ungrouped data without headers', () => { + const items: GroupedItem[] = [ + { id: 0, group: 'A' }, + { id: 1, group: 'B' }, + ]; - expect(result.current).toEqual([]); + const { result } = renderHook(() => { + const groupData = useGroupData(items); + return useFlattenData(items, groupData); + }); + + expect(result.current).toEqual({ + rows: [ + { type: 'item', item: items[0], index: 0 }, + { type: 'item', item: items[1], index: 1 }, + ], + headerRows: [], + groupKeyToItems: new Map(), + }); }); }); diff --git a/tests/listy.behavior.test.tsx b/tests/listy.behavior.test.tsx index a2a86de..9e6e6db 100644 --- a/tests/listy.behavior.test.tsx +++ b/tests/listy.behavior.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { act, render } from '@testing-library/react'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; import Listy, { type ListyRef, type ListyProps } from '@rc-component/listy'; -import type { FlattenRowsResult } from '../src/hooks/useFlattenRows'; jest.mock('rc-virtual-list', () => { const React = require('react'); @@ -66,17 +65,6 @@ type MockedVirtualListComponent = React.ForwardRefExoticComponent & { const MockedVirtualList = require('rc-virtual-list') .default as MockedVirtualListComponent; -let mockFlattenRows: FlattenRowsResult | null = null; - -jest.mock('../src/hooks/useFlattenRows', () => { - const actual = jest.requireActual('../src/hooks/useFlattenRows'); - return { - __esModule: true, - default: (items: any[], group: any, segments: any) => - mockFlattenRows ?? actual.default(items, group, segments), - }; -}); - describe('Listy behaviors', () => { beforeEach(() => { MockedVirtualList.__setExtraInfo({ @@ -85,7 +73,6 @@ describe('Listy behaviors', () => { virtual: true, }); MockedVirtualList.__setScrollHandler(() => {}); - mockFlattenRows = null; }); const renderList = ( From 95d5926c9d2e317046f0facf8a51eb07cd4941dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 15 May 2026 17:45:12 +0800 Subject: [PATCH 2/4] remove hooks barrel export --- src/hooks/index.ts | 5 ----- tests/hooks.test.tsx | 8 +++----- 2 files changed, 3 insertions(+), 10 deletions(-) delete mode 100644 src/hooks/index.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts deleted file mode 100644 index 9315775..0000000 --- a/src/hooks/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import useGroupData from './useGroupData'; -import useFlattenData from './useFlattenData'; -import useStickyGroupHeader from './useStickyGroupHeader'; - -export { useGroupData, useFlattenData, useStickyGroupHeader }; diff --git a/tests/hooks.test.tsx b/tests/hooks.test.tsx index 58a50fa..fd265b1 100644 --- a/tests/hooks.test.tsx +++ b/tests/hooks.test.tsx @@ -3,11 +3,9 @@ import { render, renderHook } from '@testing-library/react'; import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; -import { - useFlattenData, - useGroupData, - useStickyGroupHeader, -} from '../src/hooks'; +import useFlattenData from '../src/hooks/useFlattenData'; +import useGroupData from '../src/hooks/useGroupData'; +import useStickyGroupHeader from '../src/hooks/useStickyGroupHeader'; import type { StickyHeaderParams } from '../src/hooks/useStickyGroupHeader'; const PREFIX_CLS = 'rc-listy'; From a3f814540b55bf85850ef20079a40a2a3e27ef4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 15 May 2026 17:54:12 +0800 Subject: [PATCH 3/4] rename flatten hook back to rows --- src/List.tsx | 6 +++--- src/hooks/{useFlattenData.ts => useFlattenRows.ts} | 6 +++--- tests/hooks.test.tsx | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) rename src/hooks/{useFlattenData.ts => useFlattenRows.ts} (90%) diff --git a/src/List.tsx b/src/List.tsx index d4e367b..a1806f3 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -4,8 +4,8 @@ import type { ScrollTo } from 'rc-virtual-list/lib/List'; import { useImperativeHandle, forwardRef } from 'react'; import useGroupData from './hooks/useGroupData'; import type { Group } from './hooks/useGroupData'; -import useFlattenData from './hooks/useFlattenData'; -import type { Row } from './hooks/useFlattenData'; +import useFlattenRows from './hooks/useFlattenRows'; +import type { Row } from './hooks/useFlattenRows'; import useStickyGroupHeader from './hooks/useStickyGroupHeader'; import clsx from 'clsx'; import { useEvent } from '@rc-component/util'; @@ -98,7 +98,7 @@ function Listy( }); // ============================= Flat Rows ============================= - const { rows, headerRows, groupKeyToItems } = useFlattenData( + const { rows, headerRows, groupKeyToItems } = useFlattenRows( data, groupData, group, diff --git a/src/hooks/useFlattenData.ts b/src/hooks/useFlattenRows.ts similarity index 90% rename from src/hooks/useFlattenData.ts rename to src/hooks/useFlattenRows.ts index eb509bd..d16fd2d 100644 --- a/src/hooks/useFlattenData.ts +++ b/src/hooks/useFlattenRows.ts @@ -5,7 +5,7 @@ export type Row = | { type: 'header'; groupKey: K } | { type: 'item'; item: T; index: number }; -export interface FlattenDataResult { +export interface FlattenRowsResult { rows: Row[]; headerRows: { groupKey: K; rowIndex: number }[]; groupKeyToItems: Map; @@ -16,11 +16,11 @@ export interface FlattenDataResult { * When grouping is enabled, items follow the insertion order of the group map * while preserving their original indexes. */ -export default function useFlattenData( +export default function useFlattenRows( data: T[], groupData: Map[]>, group?: Group, -): FlattenDataResult { +): FlattenRowsResult { return React.useMemo(() => { const flatRows: Row[] = []; const headerRows: { groupKey: K; rowIndex: number }[] = []; diff --git a/tests/hooks.test.tsx b/tests/hooks.test.tsx index fd265b1..29b72a5 100644 --- a/tests/hooks.test.tsx +++ b/tests/hooks.test.tsx @@ -3,7 +3,7 @@ import { render, renderHook } from '@testing-library/react'; import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; -import useFlattenData from '../src/hooks/useFlattenData'; +import useFlattenRows from '../src/hooks/useFlattenRows'; import useGroupData from '../src/hooks/useGroupData'; import useStickyGroupHeader from '../src/hooks/useStickyGroupHeader'; import type { StickyHeaderParams } from '../src/hooks/useStickyGroupHeader'; @@ -128,7 +128,7 @@ describe('useGroupData', () => { }); }); -describe('useFlattenData', () => { +describe('useFlattenRows', () => { it('flattens grouped data into header and item rows', () => { const items: GroupedItem[] = [ { id: 0, group: 'A' }, @@ -142,7 +142,7 @@ describe('useFlattenData', () => { const { result } = renderHook(() => { const groupData = useGroupData(items, group); - return useFlattenData(items, groupData, group); + return useFlattenRows(items, groupData, group); }); expect(result.current.rows).toEqual([ @@ -172,7 +172,7 @@ describe('useFlattenData', () => { const { result } = renderHook(() => { const groupData = useGroupData(items); - return useFlattenData(items, groupData); + return useFlattenRows(items, groupData); }); expect(result.current).toEqual({ From 118d98e49959df9e4f584f703b4aba73874fd371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Fri, 15 May 2026 17:55:30 +0800 Subject: [PATCH 4/4] rename group hook back to segments --- src/List.tsx | 6 +++--- src/hooks/useFlattenRows.ts | 4 ++-- .../{useGroupData.ts => useGroupSegments.ts} | 14 +++++++------- src/hooks/useStickyGroupHeader.tsx | 2 +- tests/hooks.test.tsx | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) rename src/hooks/{useGroupData.ts => useGroupSegments.ts} (68%) diff --git a/src/List.tsx b/src/List.tsx index a1806f3..213ef08 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -2,8 +2,8 @@ import * as React from 'react'; import VirtualList, { type ListRef } from 'rc-virtual-list'; import type { ScrollTo } from 'rc-virtual-list/lib/List'; import { useImperativeHandle, forwardRef } from 'react'; -import useGroupData from './hooks/useGroupData'; -import type { Group } from './hooks/useGroupData'; +import useGroupSegments from './hooks/useGroupSegments'; +import type { Group } from './hooks/useGroupSegments'; import useFlattenRows from './hooks/useFlattenRows'; import type { Row } from './hooks/useFlattenRows'; import useStickyGroupHeader from './hooks/useStickyGroupHeader'; @@ -83,7 +83,7 @@ function Listy( })); // ============================= Grouping ============================= - const groupData = useGroupData(data, group); + const groupData = useGroupSegments(data, group); // ============================= Row Keys ============================= const getKey = useEvent((row: Row): React.Key => { diff --git a/src/hooks/useFlattenRows.ts b/src/hooks/useFlattenRows.ts index d16fd2d..02b9551 100644 --- a/src/hooks/useFlattenRows.ts +++ b/src/hooks/useFlattenRows.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import type { Group, GroupDataItem } from './useGroupData'; +import type { Group, GroupSegmentItem } from './useGroupSegments'; export type Row = | { type: 'header'; groupKey: K } @@ -18,7 +18,7 @@ export interface FlattenRowsResult { */ export default function useFlattenRows( data: T[], - groupData: Map[]>, + groupData: Map[]>, group?: Group, ): FlattenRowsResult { return React.useMemo(() => { diff --git a/src/hooks/useGroupData.ts b/src/hooks/useGroupSegments.ts similarity index 68% rename from src/hooks/useGroupData.ts rename to src/hooks/useGroupSegments.ts index f06113b..8611c4e 100644 --- a/src/hooks/useGroupData.ts +++ b/src/hooks/useGroupSegments.ts @@ -5,7 +5,7 @@ export interface Group { title: (groupKey: K, items: T[]) => React.ReactNode; } -export interface GroupDataItem { +export interface GroupSegmentItem { item: T; index: number; } @@ -16,12 +16,12 @@ export interface GroupDataItem { * This groups by key across the full data set and does not require items with * the same key to be contiguous. */ -export default function useGroupData( +export default function useGroupSegments( data: T[], group?: Group, -): Map[]> { +): Map[]> { return React.useMemo(() => { - const map = new Map[]>(); + const map = new Map[]>(); if (!group) { return map; @@ -30,12 +30,12 @@ export default function useGroupData( data.forEach((item, index) => { const groupKey = group.key(item); const groupItems = map.get(groupKey); - const groupDataItem = { item, index }; + const groupSegmentItem = { item, index }; if (groupItems) { - groupItems.push(groupDataItem); + groupItems.push(groupSegmentItem); } else { - map.set(groupKey, [groupDataItem]); + map.set(groupKey, [groupSegmentItem]); } }); diff --git a/src/hooks/useStickyGroupHeader.tsx b/src/hooks/useStickyGroupHeader.tsx index d0adfc8..42a8564 100644 --- a/src/hooks/useStickyGroupHeader.tsx +++ b/src/hooks/useStickyGroupHeader.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import Portal from '@rc-component/portal'; import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; -import type { Group } from './useGroupData'; +import type { Group } from './useGroupSegments'; export interface StickyHeaderParams { enabled: boolean; diff --git a/tests/hooks.test.tsx b/tests/hooks.test.tsx index 29b72a5..30a7886 100644 --- a/tests/hooks.test.tsx +++ b/tests/hooks.test.tsx @@ -4,7 +4,7 @@ import type { ListRef } from 'rc-virtual-list'; import type { ExtraRenderInfo } from 'rc-virtual-list/lib/interface'; import useFlattenRows from '../src/hooks/useFlattenRows'; -import useGroupData from '../src/hooks/useGroupData'; +import useGroupSegments from '../src/hooks/useGroupSegments'; import useStickyGroupHeader from '../src/hooks/useStickyGroupHeader'; import type { StickyHeaderParams } from '../src/hooks/useStickyGroupHeader'; @@ -65,7 +65,7 @@ const createListRef = ( } as React.RefObject; }; -describe('useGroupData', () => { +describe('useGroupSegments', () => { it('groups items by key across the full data set', () => { const items: GroupedItem[] = [ { id: 0, group: 'A' }, @@ -76,7 +76,7 @@ describe('useGroupData', () => { ]; const { result } = renderHook(() => - useGroupData(items, { + useGroupSegments(items, { key: (item) => item.group, title: () => null, }), @@ -106,7 +106,7 @@ describe('useGroupData', () => { it('supports empty states', () => { const staticGroup = { key: () => 'static', title: () => null }; const { result: staticResult } = renderHook(() => - useGroupData([{ id: 1, group: 'unused' }], staticGroup), + useGroupSegments([{ id: 1, group: 'unused' }], staticGroup), ); expect(staticResult.current).toEqual( @@ -114,12 +114,12 @@ describe('useGroupData', () => { ); const { result: noGroup } = renderHook(() => - useGroupData([{ id: 1, group: 'A' }], undefined), + useGroupSegments([{ id: 1, group: 'A' }], undefined), ); expect(noGroup.current).toEqual(new Map()); const { result: noItems } = renderHook(() => - useGroupData([], { + useGroupSegments([], { key: (item) => item.group, title: () => null, }), @@ -141,7 +141,7 @@ describe('useFlattenRows', () => { }; const { result } = renderHook(() => { - const groupData = useGroupData(items, group); + const groupData = useGroupSegments(items, group); return useFlattenRows(items, groupData, group); }); @@ -171,7 +171,7 @@ describe('useFlattenRows', () => { ]; const { result } = renderHook(() => { - const groupData = useGroupData(items); + const groupData = useGroupSegments(items); return useFlattenRows(items, groupData); });