Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

83 lines
2.8 KiB
Plaintext

---
title: Rendering Lists
description: How to efficiently render lists of items from store state.
---
## The pattern
Rendering a list of items from store state follows a three-step pattern:
1. **Subscribe to the list length** with `useAuiState` — re-renders only when items are added or removed
2. **Memoize the element array** with `useMemo` — avoids recreating elements on unrelated re-renders
3. **Use `RenderChildrenWithAccessor`** inside each item — defers reading item state until the child actually needs it
```tsx
import { useMemo } from "react";
import { useAuiState, RenderChildrenWithAccessor } from "@assistant-ui/store";
const TodoList = ({
children,
}: {
children: (value: { todo: TodoState }) => ReactNode;
}) => {
const length = useAuiState((s) => s.todoList.todos.length);
return useMemo(
() =>
Array.from({ length }, (_, index) => (
<TodoProvider key={index} index={index}>
<RenderChildrenWithAccessor
getItemState={(aui) => aui.todoList().todo({ index }).getState()}
>
{(getItem) =>
children({
get todo() {
return getItem();
},
})
}
</RenderChildrenWithAccessor>
</TodoProvider>
)),
[length, children],
);
};
```
Usage:
```tsx
<TodoList>
{({ todo }) => <TodoCard title={todo.title} />}
</TodoList>
```
## Why this pattern?
### Length-based subscription
Subscribing to `.length` instead of the full array means the list component only re-renders when items are added or removed — not when an individual item's data changes. This is significantly cheaper than subscribing to keys or the full array.
### RenderChildrenWithAccessor
`RenderChildrenWithAccessor` provides a lazy accessor (`getItem`) instead of reading item state eagerly. This means:
- **Deferred reads**: If the children render function never accesses the item, no subscription is created
- **Propless memoization**: When children returns a propless component (e.g. `{() => <Todo />}`), the output is automatically memoized — it won't re-render on parent updates
The `getItem` function is a getter that reads the latest state on demand. Wrap it in a lazy property (`get todo() { return getItem(); }`) so the state is only read when the consumer accesses it.
## Built-in primitives
All assistant-ui list primitives (Messages, Parts, Attachments, Suggestions, ThreadListItems, etc.) use this pattern internally. When using the children render function API, you get the lazy accessor behavior automatically:
```tsx
<ThreadPrimitive.Messages>
{({ message }) => {
// `message` is a lazy getter — state is read here, not above
if (message.role === "user") return <MyUserMessage />;
return <MyAssistantMessage />;
}}
</ThreadPrimitive.Messages>
```