List
List
Headless selectable list primitives with pluggable containers.
Usage
Compose list-driven selection UIs inside Menu, Popover, sheets, and custom dropdowns.
Highlights
- Single or multiple selection (controlled or uncontrolled)
- Consumer-owned rendering, filtering, grouping, and loading
- Works with any list container via `useListContextValue`
When to use it
- You need Select-like selection behavior without trigger/dropdown coupling.
- You want custom rows while keeping shared selection state.
- You want to own filtering, pagination, grouping, and virtualization outside the primitive.
Compound Components
| Component | Description |
|---|---|
List.Content | - |
List.Item | - |
Quick reference
- Selection state —
value/defaultValue/onChange, single ormultiple. IDs arestring | number. - Building blocks — use
<List.Item value>for toggle behavior, anduseListContextValuefor state-aware custom layouts. - Rendering — render anything you want inside
<List>:List.Content,FlatList,SectionList, grouped UIs, loading states, etc. - No data/search abstraction — filtering, grouping, pagination, and loading are fully consumer-owned.
Examples
Single-select with List.Content
import { List } from 'react-native-molecules/components/List'; import { Icon } from 'react-native-molecules/components/Icon'; const items = [ { id: 1, label: 'Apple' }, { id: 2, label: 'Banana' }, { id: 3, label: 'Cherry' }, { id: 4, label: 'Date' }, ]; export default function Example() { return ( <List> <List.Content style={{ maxHeight: 240 }}> {items.map(item => ( <List.Item key={item.id} value={item.id}> <Text typescale="bodyLarge">{item.label}</Text> {/* render your own right icon/layout if needed */} </List.Item> ))} </List.Content> </List> ); }
Static content
If you want to render sections, loading placeholders, or any other custom layout yourself, pass normal children to <List.Content>. Selection still works for <List.Item value>.
import { View } from 'react-native'; import { List } from 'react-native-molecules/components/List'; export default function Example() { return ( <List multiple> <List.Content style={{ maxHeight: 320 }}> <View style={{ paddingHorizontal: 16, paddingVertical: 4 }}> <Text typescale="titleSmall">Frontend</Text> </View> <List.Item value="react"> <Text typescale="bodyLarge">React</Text> </List.Item> <List.Item value="vue"> <Text typescale="bodyLarge">Vue</Text> </List.Item> <View style={{ paddingHorizontal: 16, paddingVertical: 4 }}> <Text typescale="titleSmall">Backend</Text> </View> <List.Item value="node"> <Text typescale="bodyLarge">Node.js</Text> </List.Item> <List.Item value="go"> <Text typescale="bodyLarge">Go</Text> </List.Item> </List.Content> </List> ); }
Single-select: re-clicking does not deselect
By default, single-select rows do not deselect on re-click — clicking the picked row in a "pick one and close" flow shouldn't clear the value. Multi-select keeps the toggle behavior. Override with <List allowDeselect={true | false}>.
FlatList using context
import { FlatList } from 'react-native'; import { List, useListContextValue } from 'react-native-molecules/components/List'; const ADJECTIVES = ['Red', 'Blue', 'Green', 'Dark', 'Bright', 'Wild', 'Soft', 'Cold', 'Hot', 'Old']; const NOUNS = ['Apple', 'Banana', 'Cherry', 'Mango', 'Grape', 'Peach', 'Lemon', 'Melon', 'Berry', 'Plum']; const items = Array.from({ length: 500 }, (_, i) => ({ id: i + 1, label: ADJECTIVES[i % ADJECTIVES.length] + ' ' + NOUNS[Math.floor(i / ADJECTIVES.length) % NOUNS.length] + ' ' + (i + 1), })); function VirtualizedList() { const isSelectedId = useListContextValue(state => state.isSelectedId); return ( <FlatList data={items} keyExtractor={item => String(item.id)} renderItem={({ item }) => ( <List.Item value={item.id}> <Text typescale="bodyLarge"> {item.label} {isSelectedId(item.id) ? '✓' : ''} </Text> </List.Item> )} style={{ maxHeight: 300 }} /> ); } export default function Example() { return ( <List> <VirtualizedList /> </List> ); }
FlatList with “load more”
Use FlatList to drive server-side pagination or “load more” behavior. Keep loading and paging state in your own component.
import { useEffect, useState } from 'react'; import { FlatList } from 'react-native'; import { List } from 'react-native-molecules/components/List'; const PAGE_SIZE = 25; async function fetchPage(page: number) { // Replace with real API call await new Promise(r => setTimeout(r, 300)); const start = page * PAGE_SIZE; const items = Array.from({ length: PAGE_SIZE }, (_, i) => ({ id: start + i + 1, label: `Item #${start + i + 1}`, })); const hasMore = page < 3; // pretend there are 4 pages return { items, hasMore }; } export default function Example() { const [page, setPage] = useState(0); const [items, setItems] = useState<any[]>([]); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); useEffect(() => { let cancelled = false; setLoading(true); setPage(0); fetchPage(0).then(result => { if (cancelled) return; setItems(result.items); setHasMore(result.hasMore); setLoading(false); }); return () => { cancelled = true; }; }, []); const loadMore = () => { if (loading || !hasMore) return; const nextPage = page + 1; setLoading(true); fetchPage(nextPage).then(result => { setItems(prev => [...prev, ...result.items]); setHasMore(result.hasMore); setPage(nextPage); setLoading(false); }); }; return ( <List> <FlatList style={{ maxHeight: 300 }} data={items} keyExtractor={item => String(item.id)} renderItem={({ item }) => ( <List.Item value={item.id}> <Text typescale="bodyLarge">{item.label}</Text> </List.Item> )} onEndReached={loadMore} onEndReachedThreshold={0.5} ListFooterComponent={ loading ? ( <Text style={{ padding: 16, textAlign: 'center' }}>Loading more…</Text> ) : null } /> </List> ); }
SectionList-style grouping in userland
For grouped lists, compute sections yourself and feed them into SectionList. List still owns selection, but you fully control the grouping model.
import { SectionList } from 'react-native'; import { List } from 'react-native-molecules/components/List'; const GROUPS = ['Frontend', 'Backend', 'Mobile', 'DevOps', 'Data']; const BASES = ['React', 'Vue', 'Node', 'Go', 'Rust', 'Swift', 'Kotlin', 'Python', 'Java', 'Ruby']; const items = Array.from({ length: 100 }, (_, i) => ({ id: String(i + 1), label: BASES[i % BASES.length] + ' Module ' + (i + 1), group: GROUPS[i % GROUPS.length], })); function buildSections(filteredItems: typeof items) { const byGroup = new Map<string, typeof items>(); for (const item of filteredItems) { const key = item.group; if (!byGroup.has(key)) byGroup.set(key, []); byGroup.get(key)!.push(item); } return Array.from(byGroup.entries()).map(([key, data]) => ({ key, title: key, data, })); } export default function Example() { return ( <List multiple> <SectionList style={{ maxHeight: 320 }} sections={buildSections(items)} keyExtractor={item => String(item.id)} renderItem={({ item }) => ( <List.Item value={item.id}> <Text typescale="bodyLarge">{item.label}</Text> </List.Item> )} renderSectionHeader={({ section }) => ( <Text typescale="titleSmall" style={{ paddingHorizontal: 16, paddingVertical: 4 }}> {section.title} ({section.data.length}) </Text> )} /> </List> ); }
Notes
<List.Item value>+onPress— whenvalueis set, bothonPressand the selection toggle fire on press, in that order. UseonPressfor side effects (closing a menu, analytics) and let the toggle driveonChange. SetshouldToggleOnPress={false}to suppress the toggle.