YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
Workspace▾
App & Library▾
Domain▾
Scalar▾
Model.Unit.tsx
A Unit file draws one record of a model: a card, a compact row, an avatar, a gallery tile, or a column helper for tables. Every list and relation that shows the model reuses these exports.
Open it when a list needs a new look, or a row should show another field. A Unit only draws; everything else has a file of its own:
What
Unit
Util
Template
Store
page
What the Unit does itself
Light model fields
✓
Title, status, dates: drawn as a card, a row or a tile.
usePage · l()
✓
Translation works on the server, so labels need no client code.
Link · href
✓
Navigation belongs to the Unit; the caller decides where it goes.
What it hands to another file
onClick
✓
A thin action such as edit or remove is a Util that the Unit renders.
Field.*
✓
A form is a Template, never part of a list item.
st.use · st.do
✓
✓
A larger interaction: a Util starts it and a store action runs it.
fetch.*
✓
The page loads the data and hands each record to the Unit as a prop.
✓Lives hereNot here
Words used on this page
TermDescription
cnst.Light<Model>
The slim version of a model: only the fields its constant picks for lists, plus display methods.
server component
A component without "use client". It becomes HTML on the server and ships no JavaScript.
slice
A named list query such as
inProject. Its name becomes the <Suffix> in generated names.hydrate
Putting data the server already loaded into the browser's store, so nothing is fetched twice.
ModelProps And Light Models
Type a Unit's props with
ModelProps<"article", cnst.LightArticle>. It gives the record a prop named after the model, plus className, href and a few props that list components fill in.A list renders a Unit many times, so it takes a Light model. The smallest complete Unit file:
apps/koyo/lib/article/Article.Unit.tsx
Layout.Unitis the usual container. It is a padded column that becomes a link whenhrefis set, and a plaindivwithout one.- Read only Light fields. A Light model carries just the fields its constant picks for lists, so do not assume a full-model field is there.
- Display logic lives on the Light class. A label or a check is a method such as
admin.label(), and the Unit only calls it. - Extra props extend ModelProps. Declare
interface MiniProps extends ModelProps<"article", cnst.LightArticle>right above the component.
What ModelProps gives you
The last three are filled in by
Data.ListContainer, which takes a Unit directly as its renderItem:articlecnst.LightArticlerequired
The record to draw. The prop is named by the first type argument.
classNamestring
Extra classes from the caller. Merge them last with
cn.hrefstring
Where the Unit links to. Without it,
Layout.Unit and Link render a plain div.onClick(model: L) => unknown
A click callback that a client parent can pass.
sliceSliceMeta
The slice the list belongs to, passed by
Data.ListContainer.actionsDataAction[]
Row actions (
edit, view, remove or an element), passed by Data.ListContainer.columnsDataColumn<L>[]
Which fields to show, passed by
Data.ListContainer.Unit Variants
One Unit file exports several shapes of the same model, each named by its purpose. The namespace already names the model, so it is
<Article.Unit.Card />, never ArticleCard.exportDescription
Card
The normal card for lists and grids.
MiniRow
A compact row for dense lists.
Admin.Unit.Row also carries its action buttons.Abstract
A short summary for feeds and list previews.
Gallery
An image-first tile for image grids.
Avatar
A small picture of the record, such as
User.Unit.Avatar.A compact row and an image tile from the same file:
apps/koyo/lib/article/Article.Unit.tsx
- Variants beat flags. Adding
Miniis simpler than givingCardanisCompactflag. - Actions come from a Util.
MinirendersArticle.Util.Removeand hands it only the id; the next section shows why. Imagetakes the file.file={article.cover}reads the URL, the size and the blur preview from theFilerelation.
Actions Inside Units
A Unit may show small actions such as remove, copy or a detail button. The Unit only places a small Util component; the Util owns the browser behaviour.
The Unit puts the button in a corner, next to the link rather than inside it:
apps/koyo/lib/article/Article.Unit.tsx
The Util is the client component. It takes the id, not the model:
apps/koyo/lib/article/Article.Util.tsx
- Only the button ships as JavaScript. When a page renders the card, the rest of it stays server-rendered HTML.
- A Util takes ids, not models. A model prop would cross the server-client boundary as a class instance, so
RemovePropstakesarticleId: string. - Keep the button outside the link. A click inside
<a>also follows the link, and a button there is invalid HTML, so the snippet makes the two siblings. - Forms and async workflows stay out. A form belongs in a Template, and a multi-step workflow in a store action.



A Unit file never uses client-only features. Lint rejects
"use client", React hooks such as useState, and an st import in a Unit. An onClick in a Unit breaks too, once a page renders it on the server.Load.Units And Direct Rendering
A list of Units reaches the screen in one of three ways. Pick by what the page holds:
| The page holds | Render with |
|---|---|
| ↳ What you get | |
init passed to a Zone | Load.Units |
| Loading, pagination, refresh and empty states, plus a hydrated store. | |
| An awaited list | list.map(…) |
| Plain server HTML in the first response. Common on server-rendered pages. | |
The un-awaited <model>List<Suffix> | Load.Stream |
| The list renders behind its own boundary instead of holding the route. | |
Load.Units in a Zone
Use
Load.Units when the slice has to manage loading, pagination, refresh and the empty state. It lives in a Zone, which receives the page's init:apps/koyo/lib/article/Article.Zone.tsx
renderItemdraws one row with a Unit. Passhrefhere, so the Unit itself stays reusable.renderEmptyis the empty state.Model.NewWrappermakes the button it wraps open the new form, andModel.EditModaldraws that form.
What Load.Units puts in the store
Load.Units hydrates the slice into the client store, so the generated pagination, query, sort, refresh and insight helpers keep working after the first render. Read any key with st.use.<key>():Store keyDescription
<model>List<Suffix>
The list
Load.Units draws, as it is on screen now.<model>InitList<Suffix>
The first list the server sent, kept for reset and comparison.
<model>InitAt<Suffix>
When the server built that first list.
<model>ListLoading<Suffix>
false once the list is hydrated, and true again while a refetch runs.<model>Insight<Suffix>
Insight returned with the slice, such as
count or summary values.pageOf<Model><Suffix>lastPageOf<Model><Suffix>limitOf<Model><Suffix>
Pagination state taken from the init object.
hasMoreOf<Model><Suffix>isCumulativeOf<Model><Suffix>
Whether more rows follow, and whether the list keeps rows appended by
loadMoreOf<Model><Suffix>().queryArgsOf<Model><Suffix>
The filter arguments the slice was loaded with.
sortOf<Model><Suffix>
The sort key the slice was loaded with.
Direct rendering on the server
When the page already holds the list, map it straight into Units. Nothing hydrates, and the rows are in the first response:
apps/koyo/page/project/[projectId]/_index.tsx
If the page holds the un-awaited
<model>List<Suffix> promise instead, wrap the map in Load.Stream. The list renders behind its own boundary rather than holding the route:apps/koyo/page/project/[projectId]/_index.tsx
<model>List<Suffix>holds model instances. Hand it only to server components. A Zone takes<model>Init<Suffix>instead.- Stream only small, static lists. When the same slice also feeds a Zone,
Load.Streamon the server andLoad.Unitsafter hydration build the rows twice. A large list goes throughinitinto the Zone alone.
Practical Rules
Six rules keep a Unit reusable:
- Light models for lists. Anything drawn once per row takes the Light model, not the full one.
- Accept
classNameandhref. Then the same Unit fits other layouts and other links. - Merge with
cn. Put the caller's classes last:cn("rounded-lg border", className). - Make clickable cards and rows with
Layout.UnitorLink. - Forms in Template, complex async work in Util or Store. A Unit keeps neither.
- Export variants, not flags. Add a variant per display purpose instead of piling flags onto one
Card.
Common mistakes
| Mistake, then the fix |
|---|
| ↳ Do this |
| Util.Remove article={article} |
A Util takes an id, so pass articleId={article.id}. |
| <button onClick={…}> |
| Move the handler into a Util and render that Util from the Unit. |
| export const ArticleCard |
Export Card. The namespace names the model: <Article.Unit.Card />. |
| article.content |
| A Light model has only the fields its constant picks. Add the field there, or draw it in a View. |
| await fetch.viewArticle(id) |
| A Unit never fetches. Load in the page and pass the record down as a prop. |