Akan.js
Docs
DocsConventionsReferencesCheatsheet
Akan.js
DocsConventionsReferencesCheatsheet
Akan.js

Released under the MIT License

  • Official Akan.js Consulting onAkansoftCopyright © 2026 Akan.js All rights reserved.System managed bybassman
    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.
    Introduction▾
    Quick StartFundamentalsPractice
    Tutorials▾
    Show DetailsModifying StatusInteract in ServiceDisplaying with SliceUX with PagesUsing ScalarUsing InsightRelate Data
    Core Concepts▾
    Folder RuleFile RuleFile Based RoutingData LayerApp ConfigAkan RuntimeMulti Client
    Architecture▾
    Architecture OverviewUI ArchitectureUI CompositionBusiness ServiceIn-Page AgentCSS And StylingUI Recipe LayerMobile App ArchitectureRuntime And Infra
    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.
    Introduction▾
    Quick StartFundamentalsPractice
    Tutorials▾
    Show DetailsModifying StatusInteract in ServiceDisplaying with SliceUX with PagesUsing ScalarUsing InsightRelate Data
    Core Concepts▾
    Folder RuleFile RuleFile Based RoutingData LayerApp ConfigAkan RuntimeMulti Client
    Architecture▾
    Architecture OverviewUI ArchitectureUI CompositionBusiness ServiceIn-Page AgentCSS And StylingUI Recipe LayerMobile App ArchitectureRuntime And Infra
    PreviousUI ArchitectureNextBusiness Service

    UI Composition

    Picture an order list screen. Besides the list itself, it needs six more things:
    • a skeleton while it loads
    • a placeholder when it is empty
    • a page control at the bottom
    • a modal for a new order
    • a second modal for editing one
    • a confirmation before anything is deleted
    That is six states around one array, and none of them is your product. akanjs/ui ships all six, already wired to the generated store. You write the row and the detail view; the shells around them handle loading, empty, paging, refresh and the CRUD modals.
    You write the row; the shell draws the rest
    On an order list screen you write only one row component. Load.Units repeats it, draws the page control under it and handles loading and empty states; the New button is a Model.New shell.
    This page is the inventory of those shells and the rules for combining them. Where the client boundary falls is the subject of UI Architecture.
    Words used on this page
    TermDescription
    shell
    A ready-made akanjs/ui component that draws loading, empty, paging or modal states around yours.
    slice
    A module's named list query, such as icecreamOrderInPublic. A shell takes one as slice.
    handle
    What fetch.init, fetch.view and fetch.edit return: await it, or destructure one promise per field.
    Suspense boundary
    A spot that shows a fallback first and streams its content in when the data lands.
    What akanjs/ui gives you
    ExportDescription
    Load.UnitsLoad.ViewLoad.Edit
    Data shells: seed the store from a fetch handle and render loading, empty and list states.
    Load.Stream
    Awaits one promise behind its own Suspense boundary; a resolved value renders inline.
    Model.NewModel.EditModel.SureToRemove
    Create, edit and remove modals wired to the generated store actions.
    Field
    The control for every model field type. Never a bare input for a model field.
    TabLayoutLinkImageEmpty
    Composition primitives. Tab keeps panel bodies on the server; Link adds the locale prefix.
    cn
    The only class merge, from akanjs/client. Pass the caller's className last.
    Where your own components go
    What you write splits the same way, by whether it belongs to one model:
    lib/<model>/
    Anything bound to one model. The module owns its own row, detail view, form and actions.
    ui/
    Anything reusable across models and bound to none of them.
    A component that seems to need both is really two components: one in each place.
    Reach third-party packages through a lib. A third-party package may not be imported from page/**, from a barrel, or from any module component file; re-export it through a lib first. That is why libs/shared/ui/Field.tsx extends the framework Field with Rich, Img and Map instead of each app importing an editor directly.

    The Shape Of A Model Screen

    A model almost always produces the same four screens. Users scan a list, create a record, open one, and come back to edit it. Each arrow below is a shell that already exists, so the four screens are four small files rather than four workflows.
    Index, new, view, edit
    Start
    IndexLoad.Units
    NewModel.New
    ViewLoad.View
    EditLoad.Edit
    End
    new
    submit
    pick a Unit
    edit
    submit
    back
    Model.SureToRemove
    Start
    IndexLoad.Units
    NewModel.New
    ViewLoad.View
    EditLoad.Edit
    End
    Start → Index
    Index → Newnew
    New → Viewsubmit
    Index → Viewpick a Unit
    View → Edit
    Each screen has one job, and one shell that does the heavy lifting:
    Index — find one
    Load.Units
    For discovery: search, scan, page through and choose a record.
    New — create one
    Model.New
    Controlled input through one Template and a submit action.
    View — read one
    Load.View
    Presents one record clearly, then offers the follow-up actions.
    Edit — change one
    Load.Edit
    The same Template as New, with the record's current values in it.
    The same stack under every screen
    Underneath all four screens, the same layers run in the same order, from the route down to the database:
    From the route to the database
    page()route entry
    Unit · Viewserver components
    Zone · Template · Utilclient components

    The Load Shells

    Never hand-roll a loading, empty or list state. A Load shell does three things for you:
    • It takes the handle the route fetched, so the data is already on its way before the page is sent.
    • It seeds the client store from that handle, so the generated pagination, query, sort and refresh actions keep working after hydration.
    • It renders the loading, empty and list states around your row component.
    ShellDescription
    Load.Units
    init from fetch.init<Model><Suffix>. renderItem draws a row; renderList takes the whole list.
    Load.View
    view from fetch.view<Model>. renderView is required; empty is the placeholder.
    Load.Edit
    edit from fetch.edit<Model>, or a partial. slice is required; type picks modal or form.
    Load.Stream
    of takes a promise or a value; children renders it. Takes a slice's x<Model>List<Suffix>.
    Load.Pagination

    The CRUD Modals

    Creating, editing and removing a record are three workflows every model needs and no model should implement. Each shell takes the slice it operates on, opens the module's own Template, and calls the generated store action on submit.
    Model.New: the trigger opens it, children fill it
    The trigger prop is the button on the page that opens the modal. The children of Model.New are the form fields inside the modal, not the button's label; the modal itself and its submit button come from the shell.
    apps/koyo/lib/icecreamOrder/IcecreamOrder.Util.tsx
    A Util export is named for the endpoint verb minus the model noun, so this file exports New and Remove rather than NewIcecreamOrder. Three props are worth knowing before you reach for one of these shells:

    Forms Are Store-Driven

    A Template, the module's form component, holds no state of its own. Every control reads one key of <model>Form from the store and writes it back through the generated setter. Two things follow from that:
    • A Template contains zero useState.
    • A saved draft can be restored into it, because the whole form lives in one place.
    apps/koyo/lib/icecreamOrder/IcecreamOrder.Template.tsx

    One Vocabulary, Many Screens

    Every string a user reads goes through the module's dictionary. Field labels, enum values, error messages and the model's own name are declared once as [en, ko] pairs, and components read them by key.
    That way the vocabulary lives next to the model, not scattered through whichever components happen to display it.
    apps/koyo/lib/icecreamOrder/icecreamOrder.dictionary.ts
    A component then reads text in one of three ways:
    CallDescription
    l("icecreamOrder.size")
    A field label, read from the model's dictionary.

    On this page

    UI Composition
    The Shape Of A Model Screen
    The Load Shells
    The CRUD Modals
    Forms Are Store-Driven
    One Vocabulary, Many Screens
    Two roads to fetch, one rule. The route calls fetch directly; a client component reaches it only through a store action. That one rule keeps the two paths from drifting apart.
    Day to day, four generated helpers are the whole surface you touch:
    HelperDescription
    fetch
    One call per endpoint, plus per-slice init, view and edit handles. Called server-side.
    st
    Read with st.use.*, write with st.do.*. The CRUD actions are generated.
    <Model>.*
    A module's Unit, View, Zone, Template and Util. Name by role: IcecreamOrder.Unit.Card.
    usePage
    l, l.trans and the page context. Works in a server component.
    Load.Page
    The paging control alone, and the shared SSR/CSR page loader.
    Don't wait for the slowest query
    The route destructures the handle instead of awaiting it. The init field goes to a Zone, and any leftover list promise goes to a Load.Stream:
    apps/koyo/page/(public)/icecreamOrder/_index.tsx
    • Load.Stream shows the skeleton until the list lands, then renders the total from it.
    • The Zone receives init and renders the rows behind a boundary of its own.
    • Each renders as its own data lands, so the page never waits for the slowest query.
    A list or insight never goes to a Zone. x<Model>List<Suffix> and x<Model>Insight<Suffix> resolve to hydrated model instances: class objects with methods. React Flight, the format the server uses to hand props to client components, refuses them. Consume them in a server component or inside a Load.Stream. The init field is the one shaped for the boundary.
    Never call fetch.init* from a client file. From a route it resolves before the first byte; after hydration it is two extra round-trips for a shell the browser already painted. To reload from the client, use the generated st.do.init<Model><Suffix>().
    trigger
    Replaces the default button that opens the modal. On Model.New and Model.Edit, children is the form body handed to the modal, not the label, and neither takes a className. Model.SureToRemove takes no children at all, so its trigger is the whole control.
    draft
    Form recovery, on by default. The shell saves the whole form as the user types and offers it back on the next open. The scope is the record id for an edit and the seed plus the route for a new form, per signed-in user. Secret and hidden values are never saved.
    name
    Model.SureToRemove shows this in the confirmation. With typeNameToRemove, the user must also type it back before the delete button enables.
    Never persist form values yourself. The old per-field cache and cacheKey props are deprecated and store nothing: they covered five control types, keyed on the translated label, and restored over server data. draft={false} turns recovery off, and draft="<scope>" names the scope when the context is in neither the id nor the seed.
    Pass the setter by reference. onChange={(v) => st.do.setSizeOnIcecreamOrder(v)} runs identically, but the arrow is a fresh anonymous closure. The control then emits no data-akan-action and publishes no agent tool for that field, so the in-page agent, E2E selectors and any external browser agent quietly lose it. To normalize a value, use the control's transform prop instead.
    Nested rows and files
    Nested rows
    Write one nested value by its path, and add or remove rows with the generated add<Field>OnX and sub<Field>OnX actions.
    st.do.writeOnIcecreamOrder("toppings.3.name", value)
    Image and file fields
    An image or file field is a relation to the File model, and the store generates its upload action. Never hand-roll a data-URL fallback.
    upload<Field>On<Model>(fileList)
    What a store action does, and doesn't
    Does
    • Read state and call fetch.
    • Update the loading, list and form state.
    • Write any result into state with this.set({ ... }).
    Doesn't
    • Return a value. Every action dispatches through st.do.<action>() and is typed void, so nothing can reach it.
    • Catch errors. The framework toasts the Err for you.
    • Repeat business rules. Password, permission, stock and payment rules stay in the service.
    l("icecreamOrder.modelName")
    The model's own name.
    l.trans({ en, ko })
    A one-off sentence that belongs to no model.
    usePage() resolves all three on both the server and the client, so a fully localized screen never needs a client boundary for its text.
    That same vocabulary, the same fetch and the same store back a customer web site, an admin console, a partner portal and a mobile app — those are client surfaces of one app, not separate apps, and which one a screen belongs to is a product decision before it is an infrastructure one. Multi Client covers how a basePath gives each surface its own routes, layout and permissions.
    edit
    Edit → Viewsubmit
    View → Indexback
    View → EndModel.SureToRemove
    store
    st.use · st.do
    fetchgenerated endpoint calls
    signalendpoint · slice
    service
    document
    page()route entry
    Unit · Viewserver components
    Zone · Template · Utilclient components
    storest.use · st.do
    fetchgenerated endpoint calls
    signalendpoint · slice
    service
    document