uidudevelopers
Foundations

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 callYou getUse it for
listEvents(client, { first: 20 })one page, flattened to Event[]"the latest 20" — most pages
paginate(client, ListEventsDocument, …)every page, flattenedsitemaps, generateStaticParams, exports
useQuery(ListEventsDocument, { first: 20 })one page, raw — edges[].nodeClient 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.

On this page