ListView#
ListView renders large lists efficiently using virtualization. Only visible rows exist, and they are recycled as you scroll, so a list of ten thousand rows costs about what the dozen on screen cost. Maps to UI Toolkit's ListView.
Import#
import { ListView } from "onejs-react"Basic Usage#
renderItem returns the JSX for one row. Components, hooks, context, className and event props all work inside it, exactly as anywhere else.
const items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]
function FruitList() {
return (
<ListView
itemsSource={items}
fixedItemHeight={40}
renderItem={(item: string) => <Label text={item} style={{ paddingLeft: 12 }} />}
style={{ height: 300 }}
/>
)
}A row is usually more than one element, and that costs nothing extra:
function ContactList({ contacts, onCall }) {
return (
<ListView
itemsSource={contacts}
fixedItemHeight={60}
renderItem={(contact: Contact, index: number) => (
<View style={{ flexDirection: "row", alignItems: "center", flexGrow: 1, paddingLeft: 10 }}>
<View style={{ width: 40, height: 40, borderRadius: 20, backgroundColor: contact.color }} />
<View style={{ marginLeft: 10, flexGrow: 1 }}>
<Label text={contact.name} style={{ fontSize: 16 }} />
<Label text={contact.email} style={{ fontSize: 12, color: "#999" }} />
</View>
<Button text="Call" onClick={() => onCall(index)} />
</View>
)}
style={{ height: 500 }}
/>
)
}renderItem needs onejs-react 0.1.56 or newer.
Props#
Required Props#
| Prop | Type | Description |
|---|---|---|
itemsSource | unknown[] | Array of items to display |
renderItem | (item, index) => ReactNode | JSX for one row. Use this, or the imperative pair below, not both |
Row Props (imperative alternative)#
| Prop | Type | Description |
|---|---|---|
makeItem | () => VisualElement | Factory that creates one row element |
bindItem | (element, index) => void | Populates a recycled row element |
unbindItem | (element, index) => void | Called when a row is recycled |
destroyItem | (element) => void | Called when a row element is retired |
Optional Props#
| Prop | Type | Default | Description |
|---|---|---|---|
fixedItemHeight | number | Fixed height for all rows | |
virtualizationMethod | "FixedHeight", "DynamicHeight" | "FixedHeight" | Virtualization strategy |
Selection Props#
| Prop | Type | Default | Description |
|---|---|---|---|
selectionType | "None", "Single", "Multiple" | "Single" | Selection mode |
selectedIndex | number | Currently selected index | |
selectedIndices | number[] | Selected indices (multiple) | |
onSelectionChange | (indices: number[]) => void | Selection change callback | |
onItemsChosen | (items: unknown[]) => void | Double-click/Enter callback |
Reordering Props#
| Prop | Type | Default | Description |
|---|---|---|---|
reorderable | boolean | false | Enable drag reordering |
reorderMode | "Simple", "Animated" | "Simple" | Reorder animation style |
Appearance Props#
| Prop | Type | Default | Description |
|---|---|---|---|
showBorder | boolean | false | Show border around list |
showAlternatingRowBackgrounds | "None", "ContentOnly", "All" | "None" | Zebra striping |
showFoldoutHeader | boolean | false | Collapsible header |
headerTitle | string | Header text | |
showAddRemoveFooter | boolean | false | Add/remove buttons |
How Rows Work#
The ListView keeps a small pool of row elements and moves the visible window over your data, so scrolling rebinds a row rather than building one. renderItem does not take that away. Each pooled row becomes the target of a React portal, so React owns what is inside a row while the ListView keeps doing the virtualization.
Two consequences worth knowing:
- A row is rebuilt when it is pointed at a different item. State inside a row belongs to the item, not to the recycled element, so an expanded row or a half-typed field cannot follow the pool onto a different item. Rows are cheap to rebuild; only the visible ones ever are.
- A list is blank for one frame when it first appears. The opening binds arrive while React is still committing the list itself, so the first fill lands on the next frame. Every scroll after that is on time: measured in the editor against 10,000 rows, every recycled row's JSX was committed on the frame the ListView bound it and laid out in that same frame's layout pass.
Selection#
function SelectableList({ items, onSelect }) {
const [selectedIndex, setSelectedIndex] = useState(-1)
return (
<ListView
itemsSource={items}
fixedItemHeight={50}
selectionType="Single"
selectedIndex={selectedIndex}
onSelectionChange={(indices) => {
setSelectedIndex(indices[0] ?? -1)
onSelect(items[indices[0]])
}}
renderItem={(item: Item, index: number) => (
<Label
text={item.name}
style={{ paddingLeft: 10, flexGrow: 1, color: index === selectedIndex ? "#5b9cf8" : "#e6e8ef" }}
/>
)}
style={{ height: 400 }}
/>
)
}selectionType="Multiple" works the same way, with selectedIndices and an onSelectionChange that hands you every selected index.
Imperative Rows#
makeItem and bindItem are the older API, and still the right one when a row is a single element whose text you want to set with no React in the path at all. You build the element and populate it yourself; the element is recycled, so bindItem must set every property it cares about.
<ListView
itemsSource={items}
fixedItemHeight={40}
showAlternatingRowBackgrounds="ContentOnly"
makeItem={() => new CS.UnityEngine.UIElements.Label()}
bindItem={(element, index) => {
element.text = items[index]
}}
/>Two things differ from the JSX path, because these are raw C# elements rather than React ones:
- Styles want real structs:
element.style.color = new CS.UnityEngine.Color(0.9, 0.9, 0.9, 1), since a CSS colour string is not converted here. - Reach children by position with
element.ElementAt(i), which needs no setup.Q(name)also works after a one-timeuseExtensions(CS.UnityEngine.UIElements.UQueryExtensions)(OneJS 3.1.3+).
Pass renderItem or the imperative pair, never both. TypeScript rejects a ListView that has both.
Performance Tips#
- Use fixedItemHeight when possible: it is faster than dynamic height.
- Keep a row's tree shallow. Rows are rebuilt as they recycle, so a row with fifty elements costs fifty per scrolled row.
- Hoist what does not change. A style object or a handler built inside
renderItemis rebuilt per row per bind; one defined outside is not. - Reach for the imperative pair when a row is one element and the list is enormous, which skips React on the row entirely.