Lists and queries
Render arrays of records and run search or load-more with queryContent.
A list is an array of records on a component — posts in a grid, logos in a strip, related articles. Each row is editable with the same Field, RichText, and Image components you use for the component's own fields.
Row shape
Every row is a fields object plus a plain id:
fields.posts.value[0];
// → { id: 'post-1', title: { value: 'Ship faster', name: 'title', … }, … }
Two things to notice:
idis a plain string, not an envelope. So areslug,_id, and_type.nameon a row field is the path inside the record (title,author.name) — notposts.0.title.
import { Field, Image } from '@amplifyup/sdk/react';
import type { Fields, ImageValue } from '@amplifyup/sdk/react';
type Post = { id: string; title: string; slug: string; mainImage: ImageValue };
export function LatestPosts({ fields }: { fields: Fields<{ heading: string; posts: Post[] }> }) {
const posts = fields.posts.value ?? [];
return (
<section>
<h2>
<Field field={fields.heading} />
</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>
<Image field={post.mainImage} />
<a href={`/blog/${post.slug}`}>
<Field field={post.title} />
</a>
</li>
))}
</ul>
</section>
);
}
Field is for one scalar field. Passing the list or the whole row is a type error:
// ✗ — the list itself
<Field field={fields.posts} />
// ✗ — the whole row
<Field field={post} />
// ✓ — one field on the record
<Field field={post.title} />
Read post.slug and post.id as plain strings — they are identifiers, not editable content.
Editing a list
Editing post.title on the canvas edits the post, not the page. If the same post appears in a grid and in a "related" list, one edit updates both.
What authors do where:
| Action | Where |
|---|---|
| Change a row's text or image | On the canvas |
| Add or remove records, reorder them | Composer props panel |
There is no add, remove, or drag handle on the canvas. Your component only renders the rows it is given.
Scalar lists
Arrays of strings or numbers stay a single field — tags, a multi-select. They are not records and have no rows:
// fields.tags is Field<string[]>
{
(fields.tags.value ?? []).map((tag) => <span key={tag}>{tag}</span>);
}
Runtime queries
Page resolve covers data known at Deploy. For data a visitor asks for — search, load more, a picker — use queryContent. The browser asks Amplify Up, Amplify Up asks your content source. No content credentials or query syntax live in your site.
Mark a query connection paginated in Composer and the list prop gains a sibling prop with pagination meta, including the published query spec. A list prop named posts gives you postsPagination:
'use client';
import { queryContent, nextPageSpec, searchSpec } from '@amplifyup/sdk/react';
import type { Fields, QueryPagination } from '@amplifyup/sdk/react';
export function ArticleGrid({
fields,
postsPagination,
}: {
fields: Fields<{ posts: Post[] }>;
postsPagination?: QueryPagination;
}) {
// …
}
postsPagination is { hasMore, limit, offset, nextOffset, spec }. Pass it to the helpers — never rebuild spec yourself:
// next page
const more = await queryContent({
trackingId,
route: '/insights',
spec: nextPageSpec(postsPagination),
});
// search
const hits = await queryContent({
trackingId,
route: '/insights',
spec: searchSpec(postsPagination, 'title', term),
});
trackingId is the same one you passed to AmplifyUpProvider, and route is the published path the query belongs to.
Rows come back in the same shape as list rows, so one renderer handles both:
const posts = hits ?? fields.posts.value ?? [];
return posts.map((post) => (
<article key={post.id}>
<h2>
<Field field={post.title} />
</h2>
</article>
));
Only entities published with that route can be queried, and visitors only see published content. queryContent is also available from @amplifyup/sdk/server.
When a row is editable
A row is editable when the SDK knows which record produced it. That happens on its own in the common cases:
| Rows came from | Editable in Composer? |
|---|---|
| A list prop on your component | Yes |
queryContent using nextPageSpec or searchSpec | Yes |
queryContent with a spec you assembled by hand | No |
| Records you fetched yourself and passed in as props | No |
Rows that are not editable still render their text everywhere. They just are not clickable in Composer, and in development the SDK logs once per field:
[AmplifyUp SDK] title has no write target; rendered read-only.
The fix is always upstream — give the rows a real source. Never try to patch the row yourself.
// ✓ the spec Composer published; rows stay editable
spec: searchSpec(postsPagination, 'title', term),
// ✗ hand-built spec with no connection behind it; rows render read-only
spec: { providerId: '', entity: '', filter: [], sort: [], limit: 10, offset: 0 },
Always start from postsPagination.spec. nextPageSpec and searchSpec do that for you, which keeps filters and
sort owned by Composer.
Do / Don't
| Do | Don't |
|---|---|
<Field field={post.title} /> | <Field field={fields.posts} /> or <Field field={post} /> |
key={post.id} — a plain string | post.id.value |
| render query rows with the same components as list rows | treat query rows as raw content objects |
nextPageSpec / searchSpec from the published list prop | hand-build a spec and expect editable rows |
| let the producer decide what is editable | add or copy a write target yourself |
| add and reorder records in the props panel | build add/remove controls on the canvas |