3. List and detail
Our screen lists authors and adds them, but the action hangs off a lone button, and there’s nowhere to see an author — their books, in particular. The everyday shape for this is a table on the left, details on the right, with an action bar that knows whether a row is selected. That layout has enough moving pieces to deserve a component; DataPage hands it to you whole.
The detail panel shows the selected author’s books with BooksForAuthor, the filtered observable query from the Arc tutorial’s chapter 3, and the action bar adds a book to that author with AddBook.
-
Describe the detail panel. It’s an ordinary component that receives the selected row as
item. Type it withIDetailsComponentProps<Author>soitemis the generatedAuthor, then read that author’s books with theBooksForAuthorproxy:src/AuthorDetails.tsx import type { Guid } from '@cratis/fundamentals';import type { IDetailsComponentProps } from '@cratis/components/DataPage';import type { Author } from './Authors/Author';import { BooksForAuthor } from './Books/Book'; // generated observable query proxyconst Books = ({ authorId }: { authorId: Guid }) => {const [books] = BooksForAuthor.use({ authorId });if (books.isPerforming && !books.hasData) return <p>Loading books…</p>;if (books.data.length === 0) return <p>No books yet.</p>;return (<ul>{books.data.map((book) => (<li key={String(book.id)}>{book.title}</li>))}</ul>);};export const AuthorDetails = ({ item }: IDetailsComponentProps<Author>) => (<section aria-label={`Books by ${item.name}`}><h2>{item.name}</h2><Books key={String(item.id)} authorId={item.id} /></section>);The query is scoped to one author and observable, so the list stays current while the panel is open. The panel owns its own loading and empty states because it runs its own query.
The
keymatters.DataPagekeeps the details component mounted when the selection moves to another row, and a query hook keeps its last result when its arguments change until the new result arrives. Without the key, selecting another author briefly shows the previous author’s books under the new name. Keying the child by author id mounts a fresh query per author, so the panel shows Loading books… instead. -
Add the book dialog. It is the same shape as
AddAuthor, with two required values the user doesn’t type: the author the book belongs to and a new book id. Both go ininitialValues:src/AddBook.tsx import { useState } from 'react';import { Guid } from '@cratis/fundamentals';import { CommandDialog } from '@cratis/components/CommandDialog';import { InputTextField } from '@cratis/components/CommandForm';import { AddBook } from './Books/AddBook'; // generated proxyexport const AddBookDialog = ({ authorId }: { authorId: Guid }) => {const [bookId] = useState(() => Guid.create());return (<CommandDialog<AddBook>command={AddBook}title='Add book'okLabel='Add'initialValues={{ authorId, bookId }}><InputTextField<AddBook> value={(command) => command.title} title='Title' /></CommandDialog>);};If the Arc tutorial’s
src/AddBook.tsxalready exports this component, reuse it. -
Assemble the page.
DataPagetakes the query, the columns, the menu actions, and the detail component. Selection, the resizable split, and hiding the panel when nothing is selected come built in. Menu item commands take no arguments, so the page records the selected author throughonSelectionChangeand reads it when Add book runs:src/Authors.tsx import { useState } from 'react';import { useDialog } from '@cratis/arc.react/dialogs';import { Column, DataPage, MenuItem } from '@cratis/components/DataPage';import { FaBook, FaPlus } from 'react-icons/fa6';import { AllAuthors, type Author } from './Authors/Author'; // generated proxyimport { AddAuthor } from './AddAuthor';import { AddBookDialog } from './AddBook';import { AuthorDetails } from './AuthorDetails';export const Authors = () => {const [selected, setSelected] = useState<Author | null>(null);const [AddAuthorWrapper, showAddAuthor] = useDialog(AddAuthor);const [AddBookWrapper, showAddBook] = useDialog(AddBookDialog);return (<div style={{ height: '100vh' }}><DataPagetitle='Authors'query={AllAuthors}emptyMessage='No authors yet'detailsComponent={AuthorDetails}onSelectionChange={(event) => setSelected(event.value)}><DataPage.MenuItems><MenuItemlabel='Add author'icon={FaPlus}command={() => void showAddAuthor()}/><MenuItemlabel='Add book'icon={FaBook}disableOnUnselectedcommand={() => {if (selected) {void showAddBook({ authorId: selected.id });}}}/></DataPage.MenuItems><DataPage.Columns><Column field='name' header='Name' sortable /></DataPage.Columns></DataPage><AddAuthorWrapper /><AddBookWrapper /></div>);};The icons come from
react-icons. Components depends on it internally, but addreact-iconsto your ownpackage.jsonwhen your code imports it, so strict package managers resolve it. -
Try it. Select an author and their books appear on the right. Drag the divider to resize. Add book is disabled until a row is selected —
disableOnUnselectedwires that to selection for you. With a row selected, add a book: the dialog closes on success and the new title appears in the detail panel without a refresh.
Look back at what you built
Section titled “Look back at what you built”Three chapters ago we had generated proxies and no screen. Now we have a real library back office:
- Chapter 1 — a live table from
AllAuthors, bound to an observable query with no fetch or subscription code. - Chapter 2 — add authors through
CommandDialog+useDialog, with required values ininitialValuesand the table updating itself the instant a command lands. - Chapter 3 — a
DataPagelist-and-detail layout: selection, menu actions, a resizable panel showing each author’s books, and a selection-aware Add book action.
The pattern under all of it is the same one Components exists to give you: point a component at a generated proxy, and the wiring is already done. A query becomes a live table; a command becomes a validated form; a selected row becomes a detail panel — and every one of them stays type-checked against the C# it came from.
Where to go next
Section titled “Where to go next”- Building a form and Displaying data — the two halves in full, including every field type and table option.
- A list screen with actions — the same screen as a quick recipe to copy.
- Choosing a component — a decision guide from “what’s on the screen” to the right component.
- Build a full-stack feature — the backend and this front end, assembled end to end.
You’ve built a screen, and you have the pattern for every other one. That’s Components doing its job — go build the rest of your app.