Skip to content

3. Relate your slices

An author exists to have written things. So our second feature is adding a book to an author’s catalog — and it’s the first time two slices in our app relate to each other. A book belongs to an author; we’ll model that with a plain foreign key — the author’s id on the book — and read a catalog back with a filtered query.

Here’s the shape:

writes

live

AddBook

(keyed by author)

Books

BooksForAuthor

(filtered by AuthorId)

React: author → books

  1. Strong types for the book. Same discipline as before:

    public record BookId(Guid Value) : ConceptAs<Guid>(Value)
    {
    public static BookId New() => new(Guid.NewGuid());
    }
    public record BookTitle(string Value) : ConceptAs<string>(Value)
    {
    public static implicit operator BookTitle(string value) => new(value);
    }
  2. The command writes a book, tagged with its author. AddBook is keyed by AuthorId — the author it belongs to — and writes a Book carrying that id:

    [Command]
    public record AddBook([Key] AuthorId AuthorId, BookId BookId, BookTitle Title)
    {
    public Task Handle(IMongoCollection<Book> books) =>
    books.InsertOneAsync(new Book(BookId, AuthorId, Title));
    }

The read model is just the book document — it carries the AuthorId it belongs to. The query takes the author’s id (Arc binds it from the route via [Key]) and returns only that author’s books, live:

[ReadModel]
public record Book([property: Key] BookId Id, AuthorId AuthorId, BookTitle Title)
{
public static ISubject<IEnumerable<Book>> BooksForAuthor([Key] AuthorId authorId, IMongoCollection<Book> books) =>
books.Observe(b => b.AuthorId == authorId);
}

BooksForAuthor takes an author id, so it’s a query per author. The list of authors comes from chapter 1’s AllAuthors; each row renders its own live catalog:

Catalog.tsx
import { AllAuthors } from './Authors/Author';
import { BooksForAuthor } from './Books/Book'; // generated proxies
const AuthorBooks = ({ authorId }: { authorId: string }) => {
const [books] = BooksForAuthor.use(authorId); // live, scoped to this author
return <ul>{books.data.map(b => <li key={String(b.id)}>{b.title}</li>)}</ul>;
};
export const Catalog = () => {
const [authors] = AllAuthors.use();
return (
<ul>
{authors.data.map(a => (
<li key={String(a.id)}>{a.name}<AuthorBooks authorId={String(a.id)} /></li>
))}
</ul>
);
};

Adding a book is a CommandDialog<AddBook>, exactly like RegisterAuthor — the only difference is that it carries the authorId of the author you’re adding to. Pass that in as an initial value:

AddBook.tsx
<CommandDialog<AddBook>
command={AddBook}
title="Add book"
okLabel="Add"
initialValues={{ authorId, bookId: Guid.create() }}>
<InputTextField<AddBook> value={i => i.title} title="Title" />
</CommandDialog>
  • A second slice — AddBook — that tags each book with the author it belongs to.
  • A relationship as a foreign key + a filtered query: BooksForAuthor reads exactly one author’s catalog, live, with no join.
  • A React screen that composes the two: a list of authors, each rendering its own live catalog.

Two features, related, both live. We keep saying “it stays live” — next we’ll make that concrete and watch a screen update itself the instant the data changes. Let’s make it live →