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
| Prop | Type | Default | Description |
|---|---|---|---|
each | readonly 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). |
estimatedItemSize | number | 50 | The size of rows not measured yet, in points, along the list’s axis. Close estimates make the scroll indicator and scrollToIndex more accurate. |
numColumns | number | 1 | Items per row (vertical lists): a grid. |
horizontal | boolean | false | Scrolls sideways. |
inverted | boolean | false | Lays out from the end, item 0 last (chat style); starts scrolled to the end and keeps its distance from the end as content changes. |
stickyHeaderIndices | number[] | Items that stick to the start while scrolled past (one column, not inverted). | |
ListHeaderComponent | ListSlot | Rendered before the items. | |
ListFooterComponent | ListSlot | Rendered after the items. | |
ListEmptyComponent | ListSlot | Shown instead of the items while each is empty. | |
ItemSeparatorComponent | ListSlot | Rendered between items (one column). | |
keyExtractor | (item: T, index: number) => string | item.key, item.id, or the index | The key of an item in viewability tokens. |
initialScrollIndex | number | Scrolls to this item before the first frame. | |
contentContainerStyle | ListContentStyle | Padding and gaps of the content: padding, paddingHorizontal, paddingVertical, paddingTop, paddingRight, paddingBottom, paddingLeft, gap, rowGap, columnGap (numbers). | |
style | StyleProp<ViewStyle> | The list’s own style (give it a size: flex: 1 or a height). | |
ref | YNode | ((node: YNode) => void) | See ref methods. |
ListSlot is a component (Header) or an element (<Header />).
#Loading, refreshing and viewability
| Prop | Type | Default | Description |
|---|---|---|---|
onEndReached | () => void | Called when the end comes within onEndReachedThreshold viewports: load more. | |
onEndReachedThreshold | number | 1 | How close to the end (in viewport lengths) onEndReached fires. |
refreshing | boolean | The pull-to-refresh spinner shows. | |
onRefresh | () => void | Enables pull to refresh; set refreshing while you load. | |
onViewableItemsChanged | (info: { viewableItems: ViewToken<T>[]; changed: ViewToken<T>[] }) => void | Items that became or stopped being viewable. | |
viewabilityConfig | ViewabilityConfig | When 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
| Prop | Type | Default | Description |
|---|---|---|---|
showsIndicator | boolean | true | The scroll indicator shows. |
scrollEnabled | boolean | true | The user can scroll it. |
bounces | boolean | true | Overscroll past the edges. |
pagingEnabled | boolean | false | Stops at multiples of the viewport. |
snapToInterval | number | 0 | Stops at multiples of this length (0: off). |
snapToOffsets | number[] | Stops at these offsets. | |
snapToAlignment | "start" | "center" | "end" | "start" | Which point of an item aligns with the viewport with snapToInterval. |
disableIntervalMomentum | boolean | false | A fling moves one snap point at most. |
decelerationRate | "normal" | "fast" | number | platform | How 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. |
scrollEventThrottle | number | Minimum milliseconds between onScroll events (at most one per frame anyway). | |
scrollValue | NativeValue | A native value fed with the scroll offset (main axis) on the UI thread, for scroll-linked effects. |
#Scroll events
| Event | Payload | When |
|---|---|---|
onScroll | ScrollEvent: { 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:
| Method | Description |
|---|---|
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.