Skip to main content

List

Inputs & Controls

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

ComponentDescription
List.Content-
List.Item-

Quick reference

  • Selection statevalue / defaultValue / onChange, single or multiple. IDs are string | number.
  • Building blocks — use <List.Item value> for toggle behavior, and useListContextValue for 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

Preview (Web)
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>.

Preview (Web)
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

Preview (Web)
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.

Preview (Web)
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.

Preview (Web)
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 — when value is set, both onPress and the selection toggle fire on press, in that order. Use onPress for side effects (closing a menu, analytics) and let the toggle drive onChange. Set shouldToggleOnPress={false} to suppress the toggle.