Model.View.tsx

A View file draws one record in full: the body of a detail page or a detail section. It takes the full model as a prop and only draws it.
Takes the full model
ticket: cnst.Ticket
Every field is there, including long text and nested data that a list leaves out.
Only draws
Ticket.Util.* · Ticket.Unit.*
It may render Units, Utils, Zones and its own subcomponents. Saving and deciding happen elsewhere.
Exports General
Ticket.View.General
General is the main export. A long screen adds named sections beside it.
Drawn through a Zone
renderView={(ticket) => …}
A detail Zone hands the model from the server to it through Load.View.
Words used on this page
full model
The complete model class, such as cnst.Ticket, with every field the constant declares.
light model
A slimmer class, such as cnst.LightTicket, holding only the fields a list needs.
view payload
What fetch.viewTicket(id) returns as ticketView: one record as plain data.
hydrate
Filling the client store with data the server already fetched, so no second request is sent.

View vs Unit

Both files only draw a model. They differ in how much of the model they get and in the role they play on the page.
View · one record in full
For one detail page or detail section.
Model
cnst.Ticket
Export
Ticket.View.General
Props
GeneralProps
Drawn by
Load.View → renderView
Unit · one item of many
For list rows, cards and compact summaries.
Model
cnst.LightTicket
Export
Ticket.Unit.Card
Props
ModelProps<"ticket", cnst.LightTicket>
Drawn by
Load.Units → renderItem
  • One record in detail is a View. It needs fields such as a long body, so it takes the full model.
  • The same shape repeated is a Unit. A list sends many records at once, so each row gets the light model.

Standard View Shape

Every View file starts from the same skeleton. Here is the whole file for a ticket:
apps/koyo/lib/ticket/Ticket.View.tsx
  • The main export is General. Pages and Zones reach it as Ticket.View.General.
  • Props are the full model plus a class name. GeneralProps sits right above the component, className first, then ticket: cnst.Ticket.
  • The caller's class goes last. cn("…", className) lets the page or Zone adjust width and spacing.
  • Every label goes through the dictionary. A field name is l("ticket.status"). An enum value is keyed by the enum's name, so "active" reads l("ticketStatus.active").

Full Model Detail Patterns

A View receives the full model, not the light summary, so it can draw any field the constant declares on it. Plain text fields go straight into the markup:
apps/blog/lib/article/Article.View.tsx
An enum goes through its dictionary label, and a number is formatted where it is drawn:
apps/koyo/lib/order/Order.View.tsx
  • Shared display logic goes on the Light model. A one-off toLocaleString() stays in the View. A format a Unit needs too becomes a method on LightOrder, which the full model inherits.
  • A long screen gets named sections. User.View in libs/shared exports General and Discord instead of one giant component.
  • A button inside is a Util. User.View.General renders User.Util.ChangePassword; the View places it, the Util owns the click.

Using View In Pages

A detail page starts the request with fetch.view<Model>(id) and gives the view payload to a Zone. Whether you await the call decides when the section arrives:
Destructure — streamed
const { ticketView } = fetch.viewTicket(id)
The page markup is sent while the query runs. The section fills in behind its own boundary.
await — part of the shell
await Promise.all([fetch.viewTicket(id)])
For when the page itself reads the model: a title, an id for a link, or a redirect decision.
Streamed
The usual detail page does not await, and hands the promise across as it is:
apps/koyo/page/ticket/[ticketId]/_index.tsx
  • No async, no await. The render callback is async only when its body awaits.
  • The Zone takes the promise. ClientView accepts a payload or its promise, and Load.View shows a skeleton until it lands.
Awaited
When the page needs the record itself, await the call. It resolves to an object holding ticket and ticketView:
apps/koyo/page/ticket/[ticketId]/_index.tsx
  • ticketView still goes to the Zone. Already resolved, it renders in the first HTML with no loading state.
  • ticket stays in the page. It is the hydrated model, for the link, a title or a redirect.
  • The page does not call Load.View itself. renderView is a function, and a server page cannot pass a function to a client component. The Zone sits between them for that reason.

Load.View And Store Hydration

Load.View puts the record from the view payload into the client store, then calls your renderView with the full model. A detail Zone is little more than this one call:
apps/koyo/lib/ticket/Ticket.Zone.tsx
  • Use it wherever server-fetched view data meets the store. A detail Zone, a tab layout or a reusable section all wrap the View this way.
  • Waiting and empty states are built in. A pending promise shows loading, a skeleton by default; an empty payload shows empty, an <Empty /> by default.
What it writes to the store
Before the View renders, Load.View sets four keys for the model:
<model>
The full model instance, built from the payload's <model>Obj.
<model>Loading
Set to false, so the View draws right away with no loading state.
<model>Modal
Set to "view", so a modal wrapper opens the record to read, not its edit form.
<model>ViewAt
The Date the server stamped on the payload, used to compare it with the store.
  • Newer store data wins. If the store already holds this record with a later <model>ViewAt, Load.View keeps the store's copy instead of the older payload.
  • Going back after a save loads the record again. If the navigation cache replays a payload from before the save, Load.View fetches the record again with st.do.view<Model>(id).

Practical Rules

What belongs in a View, and which file takes everything else:
The work
View
Util
Zone
page
Drawing — the View's job
fields and markup
✓
Titles, body text, nested data and formatted numbers from the full model.
l() · l.trans()
✓
Field names, enum values and headings come from the dictionary.
General · Discord
✓
A large View splits into named sections, as User.View does, not one giant General.
<Model>.Unit · <Model>.Util
✓
A View may render Units, Utils and Zones; each keeps its own job.
Behaviour — another file
onClick · submit
✓
A button or action is a Util the View renders, such as User.Util.ChangePassword.
useState · useEffect
✓
✓
Hooks need the browser, so they live in a Util or a Zone.
st.use · st.do
✓
✓
Store reads and writes. The store, signal and service do the actual mutation.
Load.View
✓
Hydrates the store from the view payload and hands the model to the View.
fetch.view<Model>
✓
Called in the route, so the query starts before the first byte is sent.
✓Belongs hereNot here
Related pages

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman