# Loading data for a block (/docs/schema/blocks/prefetch)

---
title: Loading data for a block
description: A block stores an id. `.prefetch()` turns it into the record on the way out, or runs a query of your own, once per read and batched across the page.
kind: guide
package: "@questpie/admin"
---

The renderer needs a URL and the column holds `"asset_77"`. This page covers the
three ways to close that gap on the server, what each one costs a read, and when
the work does not run at all.

## Expanding a field you already store

Name the fields to expand. Each one is fetched and placed under
`_data[blockId]`, beside the id that is still in `_values`.

```ts
.prefetch({ with: { backgroundImage: true } })
```

Only relation and upload fields expand. A name in `with` that is not one of
those is skipped in silence, never half resolved. Expansions are batched by
target collection, field kind and nested `with`, so twenty heroes carrying one
upload each cost one query, not twenty.

To go a level deeper, nest `with`. The inner object is handed straight to the
target collection's `find`, so it behaves exactly like a normal relation load.

```ts
.prefetch({ with: { author: { with: { avatar: true } } } })
```

## Running your own query

For anything you cannot reach by following an id, pass a function. It receives
the block's typed `values` and a context, and whatever it returns becomes that
block's `_data`.

```ts title="src/questpie/server/blocks/latest-news.ts"
import { block } from "#questpie/factories";

export const latestNewsBlock = block("latest-news")
	.fields(({ f }) => ({
		count: f.number().default(3),
	}))
	.prefetch(async ({ values, ctx }) => {
		const res = await ctx.collections.news.find({
			limit: values.count ?? 3,
			orderBy: { publishedAt: "desc" },
		});
		return { news: res.docs };
	});
```

`ctx` is the app context with `blockId`, `blockType` and `locale` added, so
`ctx.collections`, `ctx.db` and `ctx.session` are all in reach.

## Both at once

Pass a `loader` alongside `with` and it runs after the expansion, with the
expanded records already in hand.

```ts
.prefetch({
	with: { backgroundImage: true },
	loader: async ({ values, expanded, ctx }) => ({
		analytics: await getStats(expanded.backgroundImage?.id),
	}),
})
```

The two results are merged. Where a key appears in both, the loader wins.

## What the renderer gets

An expanded field arrives as the whole record, or `null` when nothing comes
back for that id, whether the row is gone or the caller may not read it.

```ts
data.backgroundImage; // { id: "asset_77", url: "…", filename: "hero.jpg", … } | null
```

The record is typed as `Record<string, unknown> & { id: string }`, which is why
reading a specific column in a renderer needs a cast. A field holding several
ids comes back as an array of the records that were found, with misses dropped.

<Callout type="warn" title="The array case is typed as one record">
	For a field storing several ids the runtime writes an array, while the
	inferred type still says one record or `null`. Narrow it yourself before you
	map over it.
</Callout>

## When it runs, and when it does not

Prefetch runs in the blocks field's `afterRead` hook, once per document, on
every read that returns the column and on the document a create or an update
hands back. It runs after the transaction commits, never inside it.

A read taken outside a request context, a direct database query with no app
context around it, returns the stored value untouched, so `_data` is simply
absent. Guard on it rather than assuming it.

<Callout type="info" title="A failed prefetch does not fail the page">
	If a loader throws, the error is logged, that one block's `_data` becomes
	`{ _error: "Prefetch failed" }`, and every other block still renders.
</Callout>

## Access

An upload field skips the asset collection's own read check and rides the parent
row's. The ids are content an editor put on a row the caller could already read,
so the asset comes back with it, minus any field the asset collection hides. A
plain relation gets no such treatment and goes through its own collection's read
rules, which is how a block can come back with a `null` where a private row was.

## Related

- **[Blocks](/docs/schema/blocks)** for the block file this chains onto.
- **[Loading related rows](/docs/schema/relations/loading)** for the `with`
  clause this reuses.
- **[Access control](/docs/schema/access-control)** for the read rules applied
  to an expanded relation.
