0
# Grouped Lists
1
2
Virtualization component for lists with grouped data and sticky group headers. Perfect for categorized content like contact lists, file browsers, or any scenario where data needs to be organized into sections with persistent headers.
3
4
## Capabilities
5
6
### GroupedVirtuoso Component
7
8
Specialized virtualization component that renders grouped data with sticky group headers that remain visible while scrolling through group items.
9
10
```typescript { .api }
11
/**
12
* Virtualization component for grouped lists with sticky headers
13
* @param props - Configuration options for the grouped virtualized list
14
* @returns JSX.Element representing the grouped virtualized list
15
*/
16
function GroupedVirtuoso<D = any, C = any>(props: GroupedVirtuosoProps<D, C>): JSX.Element;
17
18
interface GroupedVirtuosoProps<D, C> extends Omit<VirtuosoProps<D, C>, 'itemContent' | 'totalCount'> {
19
/** Specifies the amount of items in each group (and how many groups there are) */
20
groupCounts?: number[];
21
/** Specifies how each group header gets rendered */
22
groupContent?: GroupContent<C>;
23
/** Specifies how each item gets rendered with group context */
24
itemContent?: GroupItemContent<D, C>;
25
/** Use when implementing inverse infinite scrolling - decrease this value in combination with groupCounts changes */
26
firstItemIndex?: number;
27
}
28
29
type GroupContent<C> = (index: number, context: C) => React.ReactNode;
30
type GroupItemContent<D, C> = (index: number, groupIndex: number, data: D, context: C) => React.ReactNode;
31
```
32
33
**Usage Examples:**
34
35
```typescript
36
import React from 'react';
37
import { GroupedVirtuoso } from 'react-virtuoso';
38
39
// Basic grouped list
40
function ContactList() {
41
const groups = [
42
{ letter: 'A', contacts: ['Alice', 'Andrew', 'Anna'] },
43
{ letter: 'B', contacts: ['Bob', 'Barbara', 'Ben'] },
44
{ letter: 'C', contacts: ['Charlie', 'Catherine', 'Chris'] },
45
];
46
47
const groupCounts = groups.map(group => group.contacts.length);
48
const items = groups.flatMap(group =>
49
group.contacts.map(contact => ({ contact, letter: group.letter }))
50
);
51
52
return (
53
<GroupedVirtuoso
54
style={{ height: '400px' }}
55
groupCounts={groupCounts}
56
groupContent={(index) => (
57
<div style={{
58
padding: '8px 16px',
59
backgroundColor: '#f0f0f0',
60
fontWeight: 'bold',
61
position: 'sticky',
62
top: 0,
63
zIndex: 1
64
}}>
65
{groups[index].letter}
66
</div>
67
)}
68
itemContent={(index, groupIndex, item) => (
69
<div style={{ padding: '12px 16px', borderBottom: '1px solid #eee' }}>
70
{item.contact}
71
</div>
72
)}
73
data={items}
74
/>
75
);
76
}
77
78
// File browser with grouped folders
79
function FileBrowser() {
80
const folders = [
81
{
82
name: 'Documents',
83
files: ['report.pdf', 'notes.txt', 'presentation.pptx']
84
},
85
{
86
name: 'Images',
87
files: ['photo1.jpg', 'photo2.jpg', 'screenshot.png']
88
},
89
{
90
name: 'Downloads',
91
files: ['installer.exe', 'data.csv', 'backup.zip']
92
}
93
];
94
95
const groupCounts = folders.map(folder => folder.files.length);
96
const files = folders.flatMap(folder =>
97
folder.files.map(file => ({ file, folder: folder.name }))
98
);
99
100
return (
101
<GroupedVirtuoso
102
style={{ height: '500px' }}
103
groupCounts={groupCounts}
104
groupContent={(index) => (
105
<div style={{
106
padding: '12px 16px',
107
backgroundColor: '#e3f2fd',
108
fontWeight: 'bold',
109
borderLeft: '4px solid #2196f3',
110
display: 'flex',
111
alignItems: 'center'
112
}}>
113
π {folders[index].name}
114
</div>
115
)}
116
itemContent={(index, groupIndex, item) => (
117
<div style={{
118
padding: '8px 24px',
119
borderBottom: '1px solid #f5f5f5',
120
display: 'flex',
121
alignItems: 'center'
122
}}>
123
π {item.file}
124
</div>
125
)}
126
data={files}
127
/>
128
);
129
}
130
131
// Dynamic groups with infinite scrolling
132
function DynamicGroupedList() {
133
const [groups, setGroups] = React.useState([
134
{ title: 'Today', items: ['Item 1', 'Item 2'] },
135
{ title: 'Yesterday', items: ['Item 3', 'Item 4', 'Item 5'] }
136
]);
137
138
const loadMore = () => {
139
setGroups(prev => [
140
...prev,
141
{
142
title: `Day ${prev.length + 1}`,
143
items: Array.from({ length: 3 }, (_, i) => `Item ${prev.flatMap(g => g.items).length + i + 1}`)
144
}
145
]);
146
};
147
148
const groupCounts = groups.map(group => group.items.length);
149
const allItems = groups.flatMap((group, groupIndex) =>
150
group.items.map(item => ({ item, groupTitle: group.title, groupIndex }))
151
);
152
153
return (
154
<GroupedVirtuoso
155
style={{ height: '400px' }}
156
groupCounts={groupCounts}
157
endReached={loadMore}
158
groupContent={(index) => (
159
<div style={{
160
padding: '16px',
161
backgroundColor: '#fff3e0',
162
fontWeight: 'bold',
163
borderBottom: '2px solid #ff9800'
164
}}>
165
{groups[index].title}
166
</div>
167
)}
168
itemContent={(index, groupIndex, data) => (
169
<div style={{ padding: '12px 24px' }}>
170
{data.item}
171
</div>
172
)}
173
data={allItems}
174
/>
175
);
176
}
177
```
178
179
### Group Content Rendering
180
181
Customize how group headers are rendered with full access to styling and interactivity.
182
183
```typescript { .api }
184
/**
185
* Callback function to render group headers
186
* @param index - Zero-based index of the group
187
* @param context - Additional context passed to the component
188
* @returns React node representing the group header
189
*/
190
type GroupContent<C> = (index: number, context: C) => React.ReactNode;
191
```
192
193
**Usage Example:**
194
195
```typescript
196
// Interactive group headers
197
<GroupedVirtuoso
198
groupContent={(index) => (
199
<div
200
style={{
201
padding: '12px',
202
backgroundColor: '#f5f5f5',
203
cursor: 'pointer',
204
userSelect: 'none'
205
}}
206
onClick={() => console.log(`Clicked group ${index}`)}
207
>
208
<strong>Group {index + 1}</strong>
209
<span style={{ float: 'right' }}>({groupCounts[index]} items)</span>
210
</div>
211
)}
212
// ... other props
213
/>
214
```
215
216
### Group Item Content Rendering
217
218
Render individual items within groups with access to both item and group context.
219
220
```typescript { .api }
221
/**
222
* Callback function to render items within groups
223
* @param index - Zero-based index of the item within the entire list
224
* @param groupIndex - Zero-based index of the group containing this item
225
* @param data - The data item to render
226
* @param context - Additional context passed to the component
227
* @returns React node representing the item
228
*/
229
type GroupItemContent<D, C> = (
230
index: number,
231
groupIndex: number,
232
data: D,
233
context: C
234
) => React.ReactNode;
235
```
236
237
**Usage Example:**
238
239
```typescript
240
// Item rendering with group awareness
241
<GroupedVirtuoso
242
itemContent={(index, groupIndex, data) => (
243
<div style={{
244
padding: '8px 16px',
245
backgroundColor: groupIndex % 2 === 0 ? '#fff' : '#f9f9f9'
246
}}>
247
<span>Item {index}</span>
248
<small style={{ color: '#666' }}>Group {groupIndex}</small>
249
<div>{data.content}</div>
250
</div>
251
)}
252
// ... other props
253
/>
254
```
255
256
### Inverse Scrolling for Groups
257
258
Support for prepending groups at the top of the list, useful for chat applications or infinite scrolling scenarios.
259
260
```typescript { .api }
261
interface GroupedVirtuosoProps<D, C> {
262
/**
263
* Use when implementing inverse infinite scrolling - decrease this value
264
* in combination with groupCounts changes to prepend groups to the top
265
*
266
* The delta should equal the amount of new items introduced, excluding groups themselves.
267
* Example: prepending 2 groups with 20 and 30 items each requires decreasing by 50.
268
*
269
* Warning: firstItemIndex should be a positive number based on total items
270
*/
271
firstItemIndex?: number;
272
}
273
```
274
275
**Usage Example:**
276
277
```typescript
278
function InverseGroupedList() {
279
const [messages, setMessages] = React.useState([
280
{ date: '2023-12-01', msgs: ['Hello', 'How are you?'] },
281
{ date: '2023-12-02', msgs: ['Good morning', 'Ready for work'] }
282
]);
283
const [firstIndex, setFirstIndex] = React.useState(1000);
284
285
const loadOlderMessages = () => {
286
const newMessages = [
287
{ date: '2023-11-30', msgs: ['Previous day message 1', 'Previous day message 2'] }
288
];
289
290
const newItemCount = newMessages.reduce((sum, group) => sum + group.msgs.length, 0);
291
292
setMessages(prev => [...newMessages, ...prev]);
293
setFirstIndex(prev => prev - newItemCount);
294
};
295
296
return (
297
<GroupedVirtuoso
298
firstItemIndex={firstIndex}
299
startReached={loadOlderMessages}
300
groupCounts={messages.map(day => day.msgs.length)}
301
groupContent={(index) => (
302
<div style={{ padding: '8px', backgroundColor: '#e8f5e8', textAlign: 'center' }}>
303
{messages[index].date}
304
</div>
305
)}
306
itemContent={(index, groupIndex, data) => (
307
<div style={{ padding: '8px 16px' }}>
308
{data}
309
</div>
310
)}
311
data={messages.flatMap(day => day.msgs)}
312
/>
313
);
314
}
315
```
316
317
### Custom Group Components
318
319
Fully customize the appearance and behavior of group elements through the components prop.
320
321
```typescript { .api }
322
interface Components<Data, Context> {
323
/** Set to customize the group item wrapping element */
324
Group?: React.ComponentType<GroupProps & ContextProp<Context>>;
325
}
326
327
type GroupProps = Pick<React.ComponentProps<'div'>, 'children' | 'style'> & {
328
'data-index': number;
329
'data-item-index': number;
330
'data-known-size': number;
331
};
332
```
333
334
**Usage Example:**
335
336
```typescript
337
<GroupedVirtuoso
338
components={{
339
Group: ({ children, ...props }) => (
340
<div
341
{...props}
342
style={{
343
...props.style,
344
borderRadius: '8px',
345
margin: '4px',
346
overflow: 'hidden',
347
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
348
}}
349
>
350
{children}
351
</div>
352
)
353
}}
354
// ... other props
355
/>
356
```
357
358
## Types
359
360
```typescript { .api }
361
interface GroupItem<D> extends Item<D> {
362
originalIndex?: number;
363
type: 'group';
364
}
365
366
interface RecordItem<D> extends Item<D> {
367
data?: D;
368
groupIndex?: number;
369
originalIndex?: number;
370
type?: undefined;
371
}
372
373
type ListItem<D> = GroupItem<D> | RecordItem<D>;
374
375
interface GroupIndexLocationWithAlign extends LocationOptions {
376
groupIndex: number;
377
}
378
379
interface GroupedScrollIntoViewLocation extends ScrollIntoViewLocationOptions {
380
groupIndex: number;
381
}
382
```