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.
General▾
Interface▾
Observability▾
Performance▾
Native▾
Development▾

CRUD With Less Code

CRUD is usually the first screen you build: list records, open one, create one, edit it and remove it. In Akan the store actions and components for all five already exist, so you write the three pieces below and the pages that place them.
post.signal.ts
The slice. It decides which records this screen can read and edit.
Post.Template.tsx
Draws the form fields. Create and edit use the same one.
Post.Zone.tsx
Connects the slice to UI behaviour with the Load and Model components.
Each task maps to the components and generated store actions that do it:
TaskStore key or action
↳ Component
ListpostListInAdmin
Load.Units
OpenviewPost(id)
Model.ViewWrapper + Model.ViewEditModal
CreatenewPost → submitPost
Load.Edit · Model.New
EditeditPost(id) → submitPost
Load.Edit · Model.Edit
RemoveremovePost(id)
Model.Remove

Start With A Slice

A slice is a named window into your model: the list one screen shows. Give it a name that matches the screen, such as inPublic, inAdmin or inProject.
1. Declare the filter
Say what "public" means in the document. Each filter adds a query<Name>() method to the service:
apps/blog/lib/post/post.document.ts
2. Expose it as a slice
Then list it in the signal, one key per screen:
apps/blog/lib/post/post.signal.ts
  • queryInPublic() comes from the filter. queryAny() comes from the any filter every model has.
  • Each key names the fetch methods. inAdmin gives fetch.initPostInAdmin() and fetch.slice.postInAdmin, which the next steps use.
  • Return the query, do not shape it. Order and page size are fetch options ({ sort, page, limit }), not .sort() on the query.
Who may call what
The guards map covers the generated endpoints, and each named slice brings its own:
root
The root slice initPost(queryKey, args), which can run any filter, so it is always Admin.
get
The single-record read fetch.post(id), which viewPost and editPost also use.
cru
createPost, updatePost, removePost; the create, update, remove keys override one each.
init({ guards })
A named slice's own list, which the guards map above never reaches.

List And Open

The page loads the first rows on the server. A Zone draws them with Load.Units, and Model.ViewEditModal beside the list handles the detail view and the edit form.
1. Load in the page
The page starts the slice query and hands the promise to the Zone:
apps/blog/page/admin/post/_index.tsx
2. Draw it in a Zone
Each card opens its post, and one modal shows whichever post is open:
apps/blog/lib/post/Post.Zone.tsx
  • Why a Zone. renderItem, renderView and renderTemplate are functions, and a server page cannot pass a function to a client component.
  • One modal serves every card. Model.ViewWrapper calls st.do.viewPost(id), and the modal draws renderView.
  • Edit is built in. The modal's Edit button swaps in renderTemplate, and Save returns to the view. The ⋮ menu holds Remove; menu={false} hides it.
  • No empty or paging code. Load.Units shows <Empty /> when there are no rows and paginates by default.

Create And Edit

Use the same Template for create and edit. The shell, the component around the form, prepares postForm and saves it, so the Template only cares about fields. A form with no id is created, one with an id is updated.
Pick the shell by where the form should appear:
Load.Edit
Goes on a page of its own, such as new.tsx; the form is open as soon as the route is.
Model.New
Goes anywhere and draws its own New button; a click calls st.do.newPost().
Model.Edit
Goes anywhere and draws its own Edit button; a click calls st.do.editPost(id).
Model.ViewEditModal
Goes beside a list in a Zone; the Edit button in its detail view opens the form.
Create page
A create page seeds the form with the values a new post starts with:
apps/blog/page/admin/post/new.tsx
Edit page
An edit page loads the record on the server and hands it to the same shell:
apps/blog/page/admin/post/[postId]/edit.tsx
  • type="form" draws the form in place with its own Save button. The default, "modal", opens it in a modal.
  • slice names the list a new post joins. A create through postInAdmin lands at the top of postListInAdmin; an edit updates every list already loaded.
  • onSubmit and onCancel take a path. "back" goes back, and [postId] in an onSubmit path becomes the saved post's id.
  • The form survives accidents. The shell saves the form as the user types and offers it back on the next open. draft={false} turns that off.
Edit in a modal
To edit without leaving the screen, Model.Edit draws an Edit button and its modal together:
apps/blog/lib/post/Post.Util.tsx
  • A click loads the record. It calls st.do.editPost(postId), which fills postForm and opens the modal.
  • Button and modal in different places? Model.Edit is Model.EditWrapper (the trigger) plus Model.EditModal id={postId} (the modal). The modal alone opens nothing until editPost runs.
  • Model.New is the create twin. Same props minus modelId; partial seeds the new form.

Remove In Util

Delete buttons usually appear in many places: a card, a detail view, a menu. Put one Remove in Post.Util.tsx, next to Edit, so the Unit, View and Zone files stay simple:
apps/blog/lib/post/Post.Util.tsx
  • It asks first. A click opens a confirm modal; confirming calls st.do.removePost(postId) and shows a success toast.
  • Every list drops the row. Removal goes by id, so any slice of the model works here; fetch.slice.post is the root one.
  • On a detail page, redirect="back" or a path moves away after the removal. name fills the confirm sentence, and title, description, action replace parts of the modal.

Tips

  • Name slices after screens, not database queries. inAdmin says who looks at the list, not how it is fetched.
  • Keep the Template boring. It reads postForm and draws fields, with each setter passed by reference: onChange={st.do.setTitleOnPost}.
  • Repeated actions go to Util. Remove, publish, approve or open a dialog: write it once in Post.Util.tsx and place it anywhere.
Read next

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