Components

ListView

ListView is a scrolling list of rows that you render. Like List, it only creates the rows near the screen and reuses their cells as you scroll, so a list of thousands of items costs about as much as the visible rows. ListView adds the look: padding around each row, dividers between rows and an optional inset style.

ui-list-view.tsx Edit in Playground Open in new tab
// ListView: 1,000 recycled rows the app renders; plain or inset, with or without dividers.
import { mount, signal, Text, View } from "nsh";
import { Button, ListView } from "nsh/ui/mobile";

const people = Array.from({ length: 1000 }, (_, i) => ({
  id: i,
  name: `Person ${i + 1}`,
  role: ["Design", "Engineering", "Sales", "Support"][i % 4],
}));

function Header(props: { inset: () => boolean; dividers: () => boolean; toggleInset: () => void; toggleDividers: () => void }) {
  return (
    <View style={{ padding: 16, paddingTop: 24, gap: 8 }}>
      <Text style={{ fontSize: 22, fontWeight: "700" }}>ListView</Text>
      <View style={{ flexDirection: "row", gap: 8 }}>
        <Button class="flex-1" variant="secondary" title={props.inset() ? "Plain" : "Inset"} onPress={props.toggleInset} />
        <Button
          class="flex-1"
          variant="secondary"
          title={props.dividers() ? "No dividers" : "Dividers"}
          onPress={props.toggleDividers}
        />
      </View>
    </View>
  );
}

function App() {
  const inset = signal(false);
  const dividers = signal(true);
  return (
    <ListView
      variant={inset() ? "inset" : "plain"}
      dividers={dividers()}
      each={people}
      keyExtractor={(p) => String(p.id)}
      ListHeaderComponent={
        <Header
          inset={inset}
          dividers={dividers}
          toggleInset={() => inset.set(!inset())}
          toggleDividers={() => dividers.set(!dividers())}
        />
      }
    >
      {(person) => (
        <View style={{ flexDirection: "row", alignItems: "center" }}>
          <Text style={{ flex: 1, fontWeight: "600" }}>{person().name}</Text>
          <Text style={{ color: "#737373" }}>{person().role}</Text>
        </View>
      )}
    </ListView>
  );
}

mount(App);
import { ListView } from "nsh/ui/mobile";

#Usage

Pass the data as each and a row renderer as the child function, the same way as with List:

import { mount, Text, View } from "nsh";
import { ListView } from "nsh/ui/mobile";

const people = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Person ${i + 1}` }));

function App() {
  return (
    <ListView each={people} keyExtractor={(p) => String(p.id)}>
      {(person, index) => (
        <View style={{ flexDirection: "row", justifyContent: "space-between" }}>
          <Text style={{ fontWeight: "600" }}>{person().name}</Text>
          <Text style={{ color: "#737373" }}>{`#${index() + 1}`}</Text>
        </View>
      )}
    </ListView>
  );
}

mount(App);

The row function receives accessors: person() and index(). It runs once per cell, and the accessors change when the cell is reused for another item. Keep per-item state in your data, not in signals created inside the row. See Lists for details on recycling.

ListView fills its parent (flex: 1). Put it in a parent with a height, such as the root view or a Screen.

#Rows

ListView wraps each row in a view with 16 pt horizontal and 12 pt vertical padding and a minimum height of 48 pt. The content is centered vertically. With dividers (the default), every row except the first has a 1 pt divider at its top.

The end of the list has extra bottom padding equal to the safe area’s bottom inset. This lets the last row scroll above the home indicator.

#Variants

VariantLook
plain (default)Full-width rows on the background color.
insetRows in a rounded box with a 16 pt margin, on a muted.background page, like grouped settings. The first row has rounded top corners and the last row has rounded bottom corners.
<ListView variant="inset" each={settings} dividers={false}>
  {(item) => <Text>{item().label}</Text>}
</ListView>

#Headers, footers and list behavior

ListView accepts the data props of List. ListView sets the layout itself (estimated row size, padding), so List’s layout props such as numColumns, horizontal, estimatedItemSize and contentContainerStyle are not available.

<ListView
  each={messages()}
  keyExtractor={(m) => m.id}
  ListHeaderComponent={<Text style={{ padding: 16, fontSize: 22, fontWeight: "700" }}>Inbox</Text>}
  ListEmptyComponent={<Text style={{ padding: 16 }}>No messages</Text>}
  refreshing={loading()}
  onRefresh={reload}
  onEndReached={loadMore}
>
  {(m) => <Text numberOfLines={1}>{m().subject}</Text>}
</ListView>

#Props

PropTypeDefaultDescription
eachreadonly T[]requiredThe items.
children(item: () => T, index: () => number) => JSX.ElementrequiredRenders one row. Called once per recycled cell.
variant"plain" | "inset""plain"Full-width rows, or rows in a rounded box with a margin.
dividersbooleantrueHairlines between rows.
keyExtractor(item: T, index: number) => stringitem.key, item.id or the indexThe key of an item.
ListHeaderComponentListSlotnoneShown before the first row (a component or an element).
ListFooterComponentListSlotnoneShown after the last row.
ListEmptyComponentListSlotnoneShown instead of the rows while each is empty.
stickyHeaderIndicesnumber[]noneItems that stick to the top while scrolled past.
initialScrollIndexnumbernoneScrolls to this item before the first frame.
refreshingbooleanfalseThe pull-to-refresh spinner shows.
onRefresh() => voidnoneThe user pulled to refresh. Set refreshing while you load.
onEndReached() => voidnoneThe end of the list came near (to load more).
onEndReachedThresholdnumber1How close to the end, in viewports, onEndReached fires.
onViewableItemsChanged(info) => voidnoneItems became viewable or stopped being viewable.
viewabilityConfigViewabilityConfignoneWhen items count as viewable.
class, classList, style, testID, onLayout, refApplied to the list’s root view.

ListView has no events of its own; the list props above are its callbacks.

#Theming

PartTheme key
Page backgroundbackground (plain), muted.background (inset)
Row background (inset)background
Text colorforeground
Dividersborder
Inset box cornersradius.lg

See Theming.

#Accessibility

ListView adds no roles or labels to rows. If a row is pressable, give its view an accessibilityRole and, if needed, an accessibilityLabel.

  • List: the virtualized list ListView is built on, with full layout control.
  • Lists guide: recycling, keys, paging.
  • SelectList: a list of options with a check mark.
  • Group: a few rows in a rounded box, without virtualization.