API

nsh/list

nsh/list exports List: a virtualized list on a native scroll view. Only the rows near the viewport exist, and their cells are reused as you scroll. Which rows exist, where they are, columns, sticky headers and viewability are all computed natively, so scrolling a list of ten thousand items runs no JS except the row bindings that change when a cell is reused.

import { List, type ListProps, type ViewToken, type ViewabilityConfig } from "nsh/list";

See Lists for the guide and <List> for the element’s props.

#List

function List<T>(props: ListProps<T>): YNode;
import { mount, Text, View } from "nsh";
import { List } from "nsh/list";

const contacts = Array.from({ length: 1000 }, (_, i) => ({ id: String(i), name: `Contact ${i}` }));

function App() {
  return (
    <List each={contacts} estimatedItemSize={48} style={{ flex: 1 }}>
      {(item, index) => (
        <View style={{ height: 48, paddingHorizontal: 16, justifyContent: "center", backgroundColor: index() % 2 ? "#f8fafc" : "#fff" }}>
          <Text>{item().name}</Text>
        </View>
      )}
    </List>
  );
}

mount(App);

#Rows are recycled

children is a function that renders one cell. It runs once per cell, not once per item: when a cell scrolls out of view it is reused for another item, and item() and index() (both accessors) change. Read them inside JSX expressions ({item().name}) so the cell updates when it is reused.

#Props

ListProps<T> combines the list’s own props with the scrolling props shared with <Scroll>.

#Data and rendering

PropTypeDefaultDescription
eachreadonly T[](required)The items. Pass a signal’s value (each={items()}) to follow changes; inserting items above the visible rows keeps those rows in place.
children(item: () => T, index: () => number) => JSX.Element(required)Renders one row (see above).
estimatedItemSizenumber50The size of rows not measured yet, in points, along the list’s axis. Close estimates make the scroll indicator and scrollToIndex more accurate.
numColumnsnumber1Items per row (vertical lists): a grid.
horizontalbooleanfalseScrolls sideways.
invertedbooleanfalseLays out from the end, item 0 last (chat style); starts scrolled to the end and keeps its distance from the end as content changes.
stickyHeaderIndicesnumber[]Items that stick to the start while scrolled past (one column, not inverted).
ListHeaderComponentListSlotRendered before the items.
ListFooterComponentListSlotRendered after the items.
ListEmptyComponentListSlotShown instead of the items while each is empty.
ItemSeparatorComponentListSlotRendered between items (one column).
keyExtractor(item: T, index: number) => stringitem.key, item.id, or the indexThe key of an item in viewability tokens.
initialScrollIndexnumberScrolls to this item before the first frame.
contentContainerStyleListContentStylePadding and gaps of the content: padding, paddingHorizontal, paddingVertical, paddingTop, paddingRight, paddingBottom, paddingLeft, gap, rowGap, columnGap (numbers).
styleStyleProp<ViewStyle>The list’s own style (give it a size: flex: 1 or a height).
refYNode | ((node: YNode) => void)See ref methods.

ListSlot is a component (Header) or an element (<Header />).

#Loading, refreshing and viewability

PropTypeDefaultDescription
onEndReached() => voidCalled when the end comes within onEndReachedThreshold viewports: load more.
onEndReachedThresholdnumber1How close to the end (in viewport lengths) onEndReached fires.
refreshingbooleanThe pull-to-refresh spinner shows.
onRefresh() => voidEnables pull to refresh; set refreshing while you load.
onViewableItemsChanged(info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => voidItems that became or stopped being viewable.
viewabilityConfigViewabilityConfigWhen an item counts as viewable.
interface ViewToken<T> {
  item: T;
  key: string;
  index: number;
  isViewable: boolean;
}

interface ViewabilityConfig {
  /** Percent of the viewport an item must cover to count as viewable. */
  viewAreaCoveragePercentThreshold?: number;
  /** Percent of an item that must be visible. Without either threshold, any visible pixel counts. */
  itemVisiblePercentThreshold?: number;
  /** Milliseconds items must stay viewable before they are reported. */
  minimumViewTime?: number;
}

#Scrolling

PropTypeDefaultDescription
showsIndicatorbooleantrueThe scroll indicator shows.
scrollEnabledbooleantrueThe user can scroll it.
bouncesbooleantrueOverscroll past the edges.
pagingEnabledbooleanfalseStops at multiples of the viewport.
snapToIntervalnumber0Stops at multiples of this length (0: off).
snapToOffsetsnumber[]Stops at these offsets.
snapToAlignment"start" | "center" | "end""start"Which point of an item aligns with the viewport with snapToInterval.
disableIntervalMomentumbooleanfalseA fling moves one snap point at most.
decelerationRate"normal" | "fast" | numberplatformHow fast a fling slows down (0.998 / 0.99, or the velocity kept per millisecond).
contentInset{ top?, right?, bottom?, left? }Room around the content.
contentOffset{ x?, y? }Where it starts.
keyboardShouldPersistTaps"never" | "always" | "handled" | boolean"never"Whether a tap dismisses the keyboard ("never": the tap only dismisses it).
keyboardDismissMode"none" | "on-drag" | "interactive""none"Whether dragging dismisses the keyboard.
scrollEventThrottlenumberMinimum milliseconds between onScroll events (at most one per frame anyway).
scrollValueNativeValueA native value fed with the scroll offset (main axis) on the UI thread, for scroll-linked effects.

#Scroll events

EventPayloadWhen
onScrollScrollEvent: { x, y, contentWidth, contentHeight, width, height, zoomScale }While scrolling.
onScrollBeginDrag{ x, y }A drag starts.
onScrollEndDrag{ x, y }The finger lifts.
onMomentumScrollBegin{ x, y }A glide starts after the drag.
onMomentumScrollEnd{ x, y }The glide ends (also when a new drag catches it).
onScrollEnd{ x, y }Scrolling came to rest.

Lists also take the props every element takes (common props).

#Ref methods

A list’s ref receives a YNode with these methods:

MethodDescription
scrollToIndex({ index, animated?, viewPosition?, viewOffset? })Scrolls item index into view. viewPosition places it: 0 at the start of the viewport (default), 0.5 in the middle, 1 at the end; viewOffset shifts it further. Resolved natively with the measured (or estimated) row sizes.
scrollToOffset({ offset, animated? })Scrolls to a content offset along the list’s axis.
scrollToEnd({ animated? })Scrolls to the end of the content (the start of an inverted list).
scrollTo({ x?, y?, animated? })Scrolls to an offset.
import { Text, View, type YNode } from "nsh";
import { List } from "nsh/list";

let list!: YNode;

<View style={{ flex: 1 }}>
  <Text onPress={() => list.scrollToIndex({ index: 60, viewPosition: 0.5 })}>Jump to 60</Text>
  <List ref={list} each={items} numColumns={3} estimatedItemSize={90} style={{ flex: 1 }}
        contentContainerStyle={{ padding: 12, gap: 8 }}>
    {(item) => <View style={{ height: 90, backgroundColor: "#e0e7ff" }}><Text>{item().title}</Text></View>}
  </List>
</View>

#Examples

A chat: newest message at the bottom, older messages loaded as you scroll up.

<List
  each={messages()}
  inverted
  estimatedItemSize={44}
  style={{ flex: 1 }}
  contentContainerStyle={{ padding: 12, gap: 6 }}
  keyboardDismissMode="interactive"
  onEndReached={loadOlder}
  onEndReachedThreshold={0.5}
>
  {(m) => (
    <View style={{ alignSelf: m().mine ? "flex-end" : "flex-start", maxWidth: "75%", padding: 10, borderRadius: 14, backgroundColor: m().mine ? "#6366f1" : "#e2e8f0" }}>
      <Text style={{ color: m().mine ? "#fff" : "#0f172a" }}>{m().text}</Text>
    </View>
  )}
</List>

Pull to refresh:

const refreshing = signal(false);
const refresh = async () => {
  refreshing.set(true);
  items.set(await load());
  refreshing.set(false);
};

<List each={items()} refreshing={refreshing()} onRefresh={refresh} style={{ flex: 1 }}>
  {(item) => <Text style={{ padding: 16 }}>{item().title}</Text>}
</List>

#ListItemsProps

type ListItemsProps<T> = Pick<ListProps<T>,
  "each" | "children" | "keyExtractor" | "stickyHeaderIndices" | "initialScrollIndex"
  | "onViewableItemsChanged" | "viewabilityConfig" | "refreshing" | "onRefresh"
  | "onEndReached" | "onEndReachedThreshold"
  | "ListHeaderComponent" | "ListFooterComponent" | "ListEmptyComponent">;

The data-and-rows half of ListProps: what you pass to a component that lays out a list itself, such as ListView from nsh/ui/mobile.