Pagination & field values
Reading whole collections — list functions, paginate, useQuery — and the one shape every field value's content takes.
Two rules that apply to every domain.
Lists are Relay connections
Every list in the GraphQL API is a Relay connection: { edges: [{ node }], pageInfo: { hasNextPage, endCursor } }.
How much of that you see depends on how you read it:
| You call | You get | Use it for |
|---|---|---|
listEvents(client, { first: 20 }) | one page, flattened to Event[] | "the latest 20" — most pages |
paginate(client, ListEventsDocument, …) | every page, flattened | sitemaps, generateStaticParams, exports |
useQuery(ListEventsDocument, { first: 20 }) | one page, raw — edges[].node | Client Components |
One page — list*
list* functions take first (page size) and, where the query supports it, after (a cursor) plus
domain filters. They return the nodes only, so they can't tell you whether there is a next page — when
you need that, use paginate or the raw connection.
import { listStories } from '@uidu/client';
const latest = await listStories(client, { first: 10 });Everything — paginate
paginate walks the connection for you, threading endCursor into after until hasNextPage is
false, and returns one flat array. Pass the generated *Document for the list and tell it where the
connection sits in the result:
import { paginate, ListEventsDocument } from '@uidu/client';
const events = await paginate(client, ListEventsDocument, {
variables: { first: 100 },
extract: (data) => data.currentWorkspace?.events,
});maxPages (default 1000) bounds the loop. Every list document is exported from @uidu/client — the
client reference lists them per domain. Worked examples: Paginate every
page, Dynamic CMS routes.
In a Client Component — useQuery
useQuery runs the same documents through SWR and returns what the query
returns, connection included:
const { data } = useQuery(ListEventsDocument, { first: 20 });
const events = data?.currentWorkspace?.events?.edges?.map((e) => e?.node) ?? [];
const more = data?.currentWorkspace?.events?.pageInfo?.hasNextPage;A field value's content is always { value: … }
Field values carry the data of CMS blocks, form responses, Model items, and the custom fields of
contacts, bookings or stories. Whenever you write one, its content is an object with a value
key — never the bare value:
// ✅ every value wrapped
fieldValuesAttributes: [
{ fieldId: 'f1', content: { value: 'hello@example.com' } },
{ fieldId: 'f2', content: { value: 42 } },
{ fieldId: 'f3', content: { value: ['gid://…', 'gid://…'] } }, // multipleSelect
];
// ❌ silently loses the answer
fieldValuesAttributes: [{ fieldId: 'f1', content: 'hello@example.com' }];It holds for every field kind ({ value: true }, { value: '2026-07-27' }, { value: <doc> } for
rich text) and wherever fieldValuesAttributes appears.
A flat value fails quietly
The API doesn't reject a bare value: the record saves, its typed columns stay empty, and reading that
field later errors. @uidu/client wraps a bare content for you on every mutation and warns in
development — but other API clients won't, so send the right shape.
If you assemble fieldValuesAttributes by hand outside an SDK mutation, normalizeFieldValuesAttributes
(or normalizeFieldValueContent for one value) applies the same fix. For Model items,
toFieldValuesAttributes(model, { room: 'Blu' }) builds the array from shortnames — see
Models.
Reading is simpler: useFields turns a CMS block's fieldValues into a
{ shortname: value } map, and Model items come with fieldValuesByShortname.