# Compose the UI with fragments

Let each component declare the data it renders, then root the whole screen in one live coverage query with useRoot - no request waterfall.

You've seen [`useQuery`](/docs/client#reads-live-views) open one live query.
A real screen is usually a component tree: a route, a list, each row, and the
badge inside a row. If every component opens its own query, the screen resolves
in stages. The parent loads, then the row loads, then the badge loads. That is a
request waterfall.

Fragments are the Rindle way to keep component ownership without that waterfall:

- each component declares the fields and relationships it renders;
- the route roots the composed tree with one named query;
- `useRoot` retains that broad server coverage query;
- children receive opaque refs and read narrow local views with `useFragment`.

No child opens a new server subscription just to render data the root already
covered.

## A Fragment Is A Named Selection

A fragment is a reusable selection over one table. Put it in a React-free
`*.queries.ts` module beside the component that renders it:

```ts
// UserBadge.queries.ts
import { defineFragment } from "@rindle/client";
import type { FragmentRef } from "@rindle/client";
import { user } from "../../shared/app-def.ts";

export const UserBadgeFragment = defineFragment(user, (u) => u.select("id", "name"));
export type UserBadgeRef = FragmentRef<typeof UserBadgeFragment>;
```

The component receives an opaque `FragmentRef` and reads the fragment's data with
`useFragment`:

```tsx
// UserBadge.tsx
import { useFragment } from "@rindle/react";
import { UserBadgeFragment, type UserBadgeRef } from "./UserBadge.queries.ts";

export function UserBadge({ user }: { user: UserBadgeRef }) {
  const data = useFragment(UserBadgeFragment, user);
  if (!data) return null;
  return <span>{data.name}</span>;
}
```

`useFragment` does not open a server query. It opens a narrow local-only read for
that exact fragment and keeps the root coverage lease alive while mounted. It
returns `null` until the covered row is available locally.

## Name Your Joins Once

Fragments often cross the same joins: issue -> owner, issue -> tags, comment ->
author. Declare those joins once with `defineRelationships`:

```ts
// shared/app-def.ts
import { defineRelationships, rel } from "@rindle/client";

export const rels = defineRelationships({
  issueOwner: rel(issue, user, { ownerId: "id" }),
  issueTags: rel(issue, tag, { id: "issueId" }),
  issueComments: rel(issue, comment, { id: "issueId" }),
  commentAuthor: rel(comment, user, { authorId: "id" }),
});
```

You can still spell a correlation inline with
`sub(alias, table, { parent, child }, ...)`; named relationships just keep common
joins typo-proof.

## Compose Children With `sub`

Use `sub` to put related rows under an alias. Passing a fragment directly creates
child refs in the React-facing local data:

```ts
const IssueOwnerFragment = defineFragment(issue, (i) =>
  i
    .select("id", "title", "ownerId")
    .sub("owner", rels.issueOwner, UserBadgeFragment),
);
// local fragment data: { id, title, ownerId, owner: UserBadgeRef[] }
```

That is the usual shape for a child component that owns its own read:

```tsx
const owner = data.owner?.[0];
return owner ? <UserBadge user={owner} /> : data.ownerId;
```

If the parent owns the child payload, use an inline builder instead. This is
useful for small rows such as tag chips where the parent sorts the list and
renders the values directly:

```ts
export const TagChipFragment = defineFragment(tag, (t) => t.select("id", "name"));

export const IssueChromeFragment = defineFragment(issue, (i) =>
  i
    .select("id", "title", "status", "priority", "ownerId")
    .sub("owner", rels.issueOwner, UserBadgeFragment)
    .sub("tags", rels.issueTags, (t) => t.include(TagChipFragment).orderBy("name", "asc")),
);
// owner is refs; tags is inline TagChip data
```

A child fragment can cross joins of its own — a comment card renders its author
with the same `UserBadgeFragment` the issue row uses:

```ts
export const CommentCardFragment = defineFragment(comment, (c) =>
  c
    .select("id", "body", "createdAt")
    .sub("author", rels.commentAuthor, UserBadgeFragment),
);
export type CommentCardRef = FragmentRef<typeof CommentCardFragment>;
```

To keep child refs **and** shape the edge (ordering, a deterministic tiebreak),
pass the fragment and then an edge builder — `IssueDetailCardFragment` in the
next section orders its comment thread exactly this way.

## Share Same-Row Data With `include`

Use `include` to merge another fragment on the same table. This is how a list
card and a detail pane can share the same issue chrome:

```ts
import type { FragmentRef } from "@rindle/client";

export const IssueCardFragment = defineFragment(issue, (i) =>
  i
    .include(IssueChromeFragment)
    .select("updatedAt")
    .countAs("commentCount", rels.issueComments),
);
export type IssueCardRef = FragmentRef<typeof IssueCardFragment>;

export const IssueDetailCardFragment = defineFragment(issue, (i) =>
  i
    .include(IssueChromeFragment)
    .sub("comments", rels.issueComments, CommentCardFragment, (c) =>
      c.orderBy("createdAt", "asc").orderBy("id", "asc"),   // the fragment + edge-builder form
    ),
);
```

`include` is order-independent. If two fragments spread the same alias with
conflicting relationship shapes, Rindle throws instead of silently choosing one.
An included fragment may not add root `where`, `orderBy`, or `limit`; the named
root query owns filtering and windowing.

(`countAs(alias, rel)` above attaches a **live correlated count** as a scalar
`number` — an empty child reads `0` — maintained incrementally as children come
and go, never re-counted.)

## Root The Tree With `useRoot`

Fragments are inert until a named query includes the top fragment:

```ts
// IssueListItem.queries.ts
import { defineQuery, newQueryBuilder } from "@rindle/client";
import { schema } from "../../shared/app-def.ts";

const q = newQueryBuilder(schema);

// The validator runs on BOTH tiers, against untrusted args — validate hard.
const validateIssuesPageArgs = (raw: unknown): { limit: number } => {
  const limit = (raw as { limit: number }).limit;
  if (!Number.isInteger(limit) || limit < 1 || limit > 1000) throw new Error("bad limit");
  return { limit };
};

export const issuesPageQuery = defineQuery(
  "issuesPage",
  validateIssuesPageArgs,
  ({ limit }: { limit: number }) =>
    q.issue.orderBy("createdAt", "desc").limit(limit).include(IssueCardFragment),
);
```

The browser and the [API server](/docs/api-server) import the same `defineQuery`
value. Calling it validates args and stamps the query with its wire identity; the
authority validates the same args before resolving the authoritative AST.

Two habits worth copying: a query with **no args** needs no validator — the
two-argument form `defineQuery("rooms", () => q.room.orderBy(…))` is complete —
and every windowed query wants a **deterministic tiebreak** (a final
`.orderBy("id", "asc")` after the primary sort) so paging is stable when the sort
column ties.

In React, prefer `useRoot` for a fragment-rooted screen:

```tsx
import { fragmentKey, useRoot } from "@rindle/react";
import { IssueCardFragment, issuesPageQuery } from "./IssueListItem.queries.ts";
import { IssueListItem } from "./IssueListItem.tsx";

function IssueList() {
  const [issues, { status }] = useRoot(issuesPageQuery, { limit: 50 }, IssueCardFragment);
  const loading = status !== "complete" && issues.length === 0;

  if (loading) return <p>Loading issues...</p>;

  return (
    <ul>
      {issues.map((issue) => (
        <IssueListItem key={fragmentKey(issue)} issue={issue} />
      ))}
    </ul>
  );
}
```

Passing the fragment as the final argument switches `useRoot` into root-ref mode:
the route gets `IssueCardRef[]`, and each row component reads its own fragment:

```tsx
import { useFragment } from "@rindle/react";
import { IssueCardFragment, type IssueCardRef } from "./IssueListItem.queries.ts";

function IssueListItem({ issue }: { issue: IssueCardRef }) {
  const data = useFragment(IssueCardFragment, issue);
  if (!data) return null;

  const owner = data.owner?.[0];

  return (
    <li>
      {data.title} - {data.commentCount} comments
      {owner ? <UserBadge user={owner} /> : data.ownerId}
      {data.tags.map((tag) => <TagChip key={tag.id} tag={tag} />)}
    </li>
  );
}
```

Use root-ref mode when the route only needs to hand rows to child components.
(A query that takes **no args** drops the args slot: `useRoot(roomsQuery,
RoomCardFragment)`.)

If the route owns the root row itself, omit the fragment argument. Here
`issueDetailQuery` is a second named query — a singular (`.one()`) root over the
detail fragment. Args are whatever shape its validator accepts; this one takes
the bare issue id:

```ts
// IssueDetail.queries.ts
export const issueDetailQuery = defineQuery(
  "issueDetail",
  (raw): string => {
    if (typeof raw !== "string" || raw.length === 0) throw new Error("bad issue id");
    return raw;
  },
  (id: string) => q.issue.where("id", "=", id).one().include(IssueDetailCardFragment),
);
```

```tsx
function IssueDetail({ issueId }: { issueId: string }) {
  const [issue, { status }] = useRoot(issueDetailQuery, issueId);

  if (!issue) {
    return <p>{status === "complete" ? "Issue not found." : "Loading issue..."}</p>;
  }

  return (
    <section>
      <h1>{issue.title}</h1>
      {issue.comments.map((comment) => (
        <CommentCard key={fragmentKey(comment)} comment={comment} />
      ))}
    </section>
  );
}
```

Without a root fragment argument, `useRoot` returns the query's local
React-facing data. Any child relationship composed with a fragment still becomes
refs; inline child builders remain inline data. (`CommentCard` reads its ref with
`useFragment(CommentCardFragment, comment)`, exactly like `UserBadge` above.)

## Loading And Masking

`useRoot` returns `[data, details]`. `details.status` is the server coverage
state:

- `unknown` means the root coverage query is not server-authoritative yet;
- `complete` means the server has answered;
- for `.one()` queries, `complete` plus `null` means not found.

`useFragment` returns `null` until its covered local row is available. In SSR,
preloaded root snapshots let `useRoot` and descendant `useFragment` calls render
from the same first-paint seed.

The moment a fragment names columns with `select(...)`, its data type narrows to
exactly those columns. A fragment with no `select` keeps the full row. Child
fragments passed directly to `sub` are opaque refs, so the parent cannot
accidentally read child-owned fields. The selected columns are also the columns
the root coverage query asks the authority to sync.

## Keep Query Modules React-Free

`*.queries.ts` modules should import `@rindle/client`, schema/relationship
definitions, validators, and sibling fragments. They should not import `.tsx`
components. This lets the browser, API authority, and SSR loader import the same
named queries without dragging React into the server graph.

That shared module graph is what keeps first paint and live hydration aligned:
the route loader preloads the same named root query that `useRoot` retains after
hydration.

## Next Steps

- [The browser client](/docs/client) - `useRoot`, `useQuery`, optimistic writes,
  and the store the root query materializes against.
- [The API server](/docs/api-server) - named query registration and authority-side
  AST resolution.
- [Server rendering](/docs/ssr) - preload the same composed root query for first
  paint, then hand off to the live browser client.
- [Full app: the issue tracker](/docs/example-issue-tracker) - fragments reused
  across list, board, detail, and activity-feed roots.
- [Supported query shapes](/docs/supported-queries-ts) - what `select`, `sub`,
  `include`, and `countAs` express today.

---

[View this page on Rindle](https://rindle.sh/docs/fragments)
