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.Util.tsx
A Util file holds a module's small client components, each doing one action: a remove button, a toolbox, a dialog trigger, a filter control or a back link.
Clicks and store actions gather here, so Unit and View stay server-rendered and Page, Zone and Template keep to their own jobs.
Always a client file
"use client";"use client" goes on line 1 by file role. A Util exists to handle a click, a hook or the store.
Named after its action
Project.Util.RemoveName it after the verb without the model, such as Remove, Resolve or SetOrg. The namespace adds the model.
Takes ids, not models
projectId: stringA model prop would cross the server-client boundary as a class instance. Read the rest from the store.
Calls, never decides
st.do.resolveReport(reportId)It calls a store action or a Model wrapper. Who may act and what changes is decided by the service and document.
Words used on this page
TermDescription
Model.EditModel.Remove
Components from
akanjs/ui that run a module's generated edit or remove flow for you.st.dost.use
The client store:
st.do.x() runs an action, and st.use.x() reads a key and re-renders on change.fetch.slice.<name>
Slice metadata that tells a wrapper which model and list to act on. It sends no request.
query args
The arguments a slice list was loaded with, such as the project ids that filter a ticket list.
File Convention
Every Util file has the same shape.
akan create-module product writes the first one for you, with a single Remove export:apps/koyo/lib/product/Product.Util.tsx
fetch.slice.productsends no request. It is slice metadata that tellsModel.Removewhich model to remove.l("base.remove")is a shared label. Words every module shares live underbase.*; a module's own words live under<model>.*.- Only
react*packages import directly.react-iconsis fine; any other third-party package reaches a Util through a lib re-export.
The rules in the file
PartDescription
lib/<model>/<Model>.Util.tsx
Sits beside the module's other files. A service module may have one; a scalar module may not.
"use client"
Always line 1, above the imports. Template and Zone carry it too; Unit and View never do.
RemoveToolboxSetOrgQueryMakerInSelfBackButton
Named exports only. Callers write
Project.Util.Remove, so no name repeats the model.interface RemoveProps
Declared right above its component and named after it. It takes ids and plain values.
@apps/<app>/client
One flat import for
fetch, st and usePage. UI pieces come from akanjs/ui.Model Wrapper Actions
Most Utils are thin controls around the Model wrappers. A toolbox gathers several of them, so the Unit or Zone that shows it stays small.
WrapperDescription
Model.Edit
Draws an Edit button that opens its Template child in an edit modal.
Model.Remove
Its children become the trigger. It asks for confirmation, then removes the record.
Model.SureToRemove
A stricter remove that shows the record's
name. typeNameToRemove makes the user retype it.A project toolbox in a dropdown menu. Only the owner sees the remove item:
apps/koyo/lib/project/Project.Util.tsx
- The wrappers call the generated actions.
Model.Editrunsst.do.editProjectandModel.SureToRemoverunsst.do.removeProject, so the Util writes no handler for them. - A custom action declares its own
st.tool. The archive button calls the tool's callable with the id, so a click and the agent run one handler. - Owner-only items use
cond ? … : null. TheisOwnerprop makes the condition visible to whoever renders the toolbox.
Dialog And Modal Actions
When an action needs a confirmation or a small input first, its dialog lives in the same Util. First decide where the open state lives:
Inside the dialog
<Dialog> + useStateDialog opens and closes itself. useState holds a draft value that only this dialog uses.In the store
st.use.reportModal()edit<Model>(id, { modal }) writes the <model>Modal key, so any component or action can open or close it.Local state: SetOrg
SetOrg picks an organization in a dialog, then saves it to the business license:
apps/koyo/lib/bizLicense/BizLicense.Util.tsx
useStateis fine here. The picked id is a draft that belongs to this dialog alone. Server data never goes inuseState.Field.ParentIdpicks a related record. It loads its options fromfetch.slice.orgInSelfand hands the chosen id toonChange.Dialog.Actionfills the footer. The save button stays disabled until an organization is picked.
Store state: Resolve
Resolve keeps the modal key in the store, so a store action opens the modal:
apps/koyo/lib/report/Report.Util.tsx
editReport(id, { modal })loads the record and names the modal. It fillsreportFormand setsreportModalto the name you pass.- The key carries the id.
resolve-${reportId}keeps each row's modal apart when a list renders many Resolve buttons. resetReportcloses it. It clearsreport,reportFormandreportModal; hand it toonCancelas is.
Query And Route Helpers
Filter controls and route-aware helpers are Utils too. They read store or route state, then call a generated action or a router helper.
What they useDescription
st.use.queryArgsOf<Model><Suffix>()
The args the slice list was last loaded with, as an array in the slice's arg order.
st.do.setQueryArgsOf<Model><Suffix>(...args)
Takes one value per slice arg, then reloads the list and insight from page 1.
st.use.path()
The current path without the locale prefix, such as
/board/abc/post/1.Link.Back
A wrapper from
akanjs/ui whose click calls router.back().Changing a filter
QueryMakerInSelf keeps the project filter of the ticketInSelf list and clears its assignee filter:
apps/koyo/lib/ticket/Ticket.Util.tsx
- Spread the args, one per slice arg. Wrapping them in one array would put the whole array into the first arg.
- An updater works too.
setQueryArgsOfTicketInSelf((projectIds, userIds) => [projectIds, []])derives the next args from the current ones.
Reading the route
BackButton shows a back link only on pages under one board:
apps/koyo/lib/board/Board.Util.tsx
{ agent: false }keeps the key off the agent's surface. The path only decides what to draw, so the in-page agent has no reason to read it.- An early
return nullis a guard clause. Use it only to bail out like this; elsewhere writecond ? <X /> : null.
Rules And Common Mistakes
What belongs in a Util, and which file takes everything else:
The work
Util
View
Unit · View
Form
Template
Logic
store · service
The Util's job
onClick → st.do.*
✓
A button that runs one store action.
Model.Edit · Model.Remove
✓
Wrappers that open the generated edit and remove flows.
Dialog · Modal
✓
A dialog trigger, and the draft value only that dialog uses.
setQueryArgs Of…
✓
Filter controls that change a slice list's query args.
st.use.path · Link.Back
✓
Helpers that read the route to decide what to show.
Another file's job
fields and markup
✓
A Unit draws one row, and a View draws one record in full.
Field.* · <model>Form
✓
A form whose fields are bound to the store.
who may act, what changes
✓
Business rules run on the server, in the service and document.
multi-step async flow
✓
A store action that the Util calls in one line.
✓Belongs hereNot here
Writing a Util
- Labels go through
l. Take it fromusePage()and writel("model.key")orl.trans({ en, ko }), never hard-coded action text. - Call, do not decide. Call
st.doactions or Model wrappers, and keep business rules in the service and document. useStateis for UI-only values. An open dialog, a selected option or a draft input qualifies; server data does not.- Keep props explicit. The caller should see which id, slice, role or name the action depends on.
- Split big toolboxes. Break a large toolbox or workflow modal into named exports instead of hiding too much in one component.
- Publish custom buttons to the agent. Model wrappers declare their own agent tools; a plain button publishes nothing until you declare
st.tool(…)beside it and hand its callable toonClick.
Common mistakes
| Mistake |
|---|
| ↳ Instead |
| {isOwner && <Remove />} |
Write isOwner ? <Remove /> : null, the house form for conditional render. |
| onChange={(v) => st.do.setNameOnX(v)} |
| Pass the setter by reference; the arrow hides the field from the agent and fails lint. |
| useEffect(() => { … }, []) |
Load data in the page and pass it down; akan quality ssr flags a mount-time load. |
| fetch.initTicketInSelf() |
Lint rejects fetch.init* in a client file. Reload with st.do.initTicketInSelf(). |
| <div>…markup only…</div> |
| A Util with no click, hook or store is server work. Move it to a Unit or View. |



A Util prop cannot be a
cnst model. A prop such as report: cnst.Report fails akan lint. Take reportId: string and read the model from the store; an enum value such as cnst.ProjectRole["value"] is still fine.Related pages