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.
CLI Reference▾
AkanJS Reference▾
akanjs/webkit
akanjs/webkit holds React hooks and helpers that run only in the browser or in the native app. Timers, lazy loading, promise state, device features and the CSR router state live here.import { lazy, useDebounce, useInterval } from "akanjs/webkit";On This Page
ExportDescription
Loads a component's code on demand.
ssr: false skips server rendering.Runs a callback once, after the calls stop for a while.
Runs a callback on a fixed interval and stops on unmount.
Runs a callback at once, then ignores calls for a while.
Tells a client component whether a promise has resolved, and its value.
Camera and location through the native runtime's plugins, with a browser fallback and the permission prompts.
Not in this module: the push hook lives in
@libs/util/webkit.In-app purchase on the native iap plugin, verified by your server. Imported from
akanjs/webkit/usePurchase.The CSR router's location parser and history stack.
The argument of the shared store's
login action.Other Exports
ExportDescription
useBodyScrollLock(active)
Locks
document.body scrolling while active. Overlays share one count.useEscapeKey(active, onEscape)
Calls
onEscape on Escape while active. Only the topmost open surface gets the key.usePageFocusEffect(effect, deps)
Runs
effect while the user is on this page and cleans it up when they leave. A page kept under the current one for a swipe back stays mounted, so a plain effect there keeps running.usePageLocation()
{ pathname, params, searchParams } of this page. st.use.searchParams() follows the page on screen, so a page being prepared or kept under the current one reads its own here.usePageActivity()
"current" | "prev" | "pending" | "hidden" — where this page stands in the CSR stack; always current outside one.usePageTooluseScreenScope
Show a pager and on-screen items to the in-page agent.
Load.Units and Load.View call them.createRobotPagecreateSitemapPage
Build robots rules (
disallow: "/admin/" by default) and a list of sitemap entries.bootCsrreplacePagesuseCsrValues
Start the CSR (mobile) bundle, swap its route modules in place during dev, and hold its router state. The generated entry calls them.
LoginAuthScreenScopeItem
Types:
"user" | "admin" | "public", and one on-screen item { id, label? }.- Call the hooks from a client file. They are React hooks, so they need a file with
"use client". Putlazy()in theui/<Folder>/index_.tsxboundary file. - Push and speech come from the util library.
usePushNotificationanduseSpeechare imported from@libs/util/webkit, not from here.
lazy
lazy is React's lazy with Akan's server switch. With ssr: false it skips server rendering, which is what a map, chart or 3D library that touches window on import needs.lazy(loader, { ssr, suspense, loading })ssrbooleandefault true
false skips server rendering: the server sends loading, and the chunk loads after mount.suspensebooleandefault false
Wraps the component in its own Suspense boundary, so only this spot waits for the chunk.
loading() => ReactNode
The placeholder. It shows only with
ssr: false or suspense: true.What Each Call Renders
| Call | Server HTML |
|---|---|
| ↳ While the chunk loads | |
| No option | The component itself. |
| The nearest boundary, usually the whole route, shows its fallback. | |
suspense: true | The component, sent after the shell in stream mode. |
Only this spot shows loading. | |
ssr: false | Only loading, or nothing. |
loading until the component mounts and its chunk arrives. | |
A globe that needs the browser, behind its lazy boundary file:
apps/myapp/ui/Globe/index_.tsx
- Keep the file pair.
"use client"andlazy()go inindex_.tsx; theindex.tsxbeside it stays safe to import on the server. Merging the two breaks server rendering. - The loaded file exports the component as default. A loader may also return the component itself, such as a package's default export.
- Use
suspense: trueonly for what opens later. A modal body, a dropdown or an editor. On a page body it moves markup out of the shell that SEO snapshots and prerendering read.
useDebounce
useDebounce returns a callback that runs only after the calls stop. A search box that queries once the user stops typing is the usual case; an image editor or a costly field update uses it while the user drags or types.useDebounce(callback, states = [], wait = 100)callback(...args) => unknownrequired
Runs once, with the arguments of the last call.
statesunknown[]default []
The dependency list of the inner
useCallback: every prop and state the callback reads.waitnumberdefault 100
Milliseconds to wait after the last call.
A search box that reloads a slice 300 ms after the last keystroke:
apps/myapp/lib/product/Product.Util.tsx
- List what the callback reads in
states. With[], the callback from the first render keeps running and sees a staleshopId. - The argument order differs from
useThrottle. Here the dependencies come second and the wait third.
useInterval
useInterval runs a callback every delay ms and clears the timer on unmount. Use it to poll a dashboard, a game state or a build log.useInterval(callback, delay)callback() => void | Promise<void>required
Runs on every tick. The callback from the latest render is the one that runs.
delaynumberrequired
Milliseconds between ticks. Changing it restarts the timer.
A Zone that refreshes its order list every 3 seconds:
apps/myapp/lib/order/Order.Zone.tsx
- A new callback each render is fine. The timer keeps running and picks up the latest callback; only a new
delayrestarts it. - Ticks do not wait for each other. An async callback that takes longer than
delayoverlaps with the next tick. - For changes as they happen, declare a
.live()slice. Polling only reloads on a timer; a live slice sends each change to its subscribers.
useThrottle
useThrottle returns a callback that runs at once, then drops calls until delay ms pass. Use it for scroll, pointer, resize and drag handlers that fire too often.useThrottle(func, delay = 200, deps = [])func(...args) => unknownrequired
Runs at once on the first call of each window.
delaynumberdefault 200
Milliseconds during which later calls are dropped.
depsunknown[]default []
Extra dependencies.
func and delay are already included.Compared With useDebounce
| Call |
|---|
| ↳ When it runs |
| useDebounce(callback, states = [], wait = 100) |
Once, after the calls stop for wait ms. |
| useThrottle(func, delay = 200, deps = []) |
At once, then drops calls for delay ms. |
A drag pad that updates its dot at most every 100 ms:
apps/myapp/ui/DragPad.tsx
- The first call always runs. Calls inside the window are dropped, not queued, so the last position of a fast drag may be skipped.
- Need the final value? Use
useDebounce. It runs once with the arguments of the last call.
useFetch / useFetchFn
These hooks turn a promise into
{ fulfilled, value } inside a client component. Use them for a value that exists only in the browser, or when client code needs the value itself, not just to draw it.hookDescription
useFetch(promiseOrValue, { onError })
Follows the promise handed on each render; a plain value comes back at once,
fulfilled: true.useFetchFn(factory, deps = [], { onError })
Calls
factory inside useMemo, so a new request starts only when deps change.Result And Option
fulfilledboolean
true once the current promise resolves; a rejected or newly handed one reads false.valueT | null
The current promise's resolved value,
null until it resolves.onError(err: string) => void
Option. Called with
"Error: <message>" when the promise rejects.The storage quota exists only in the browser, so this component is loaded through
lazy(…, { ssr: false }):apps/myapp/ui/StorageUsage/StorageUsage.tsx
- To draw a promise, reach for
<Load.Stream of={promise}>first. It streams real markup from the server, whileuseFetchwaits in an effect and ships only the fallback in the first HTML. - Do not call
fetch.*on mount. Load server data in the route and hand it down as a prop or an unawaited promise. - The factory runs during render, on the server too. A client component still renders once on the server, where
navigator.storageis missing, so a browser-only call needs thessr: falseboundary shown above. - It follows the promise handed on each render. A new promise, or new
depsonuseFetchFn, resets the result tofulfilled: falseuntil it resolves, and a result from an older promise is dropped.


Never create the promise inline in render.
useFetch(navigator.storage.estimate()) makes a new promise on every render, and each result renders again, so the request repeats in a loop. Pass a stable promise, such as a prop or a ref, or use useFetchFn(factory, deps), which recreates it only when deps change.useCamera
useCamera takes a photo or picks one from the library through the native runtime's camera plugin. In the native app it asks for the camera first and opens the app settings when the user has denied it; in a browser it picks from files.useCamera({ promptLabels } = {})ReturnsDescription
getPhoto(src = "prompt")
Takes or picks one photo as
{ dataUrl }, an upright JPEG. "prompt" shows a camera-or-library sheet in the native app; cancel returns undefined.pickImage({ limit })
Picks several images from the library, each as
{ dataUrl }.permissions
{ camera }. Read on mount in the native app, "prompt" until then.checkPermission()
Asks for the camera, and opens the app settings when it is denied.
Option
promptLabels{ header?, photo?, picture?, cancel? }default {}
Text of the native picker sheet. A missing one comes from the
base dictionary.A button that takes a photo and previews it:
apps/myapp/ui/TakePhoto.tsx
- Declare the permission.
"camera"innative.permissionsadds the camera plugin and its usage texts. There is no package to install. - The browser picks from files. Outside the native app every source becomes the library, so the same call works on the web without a check of your own.
- The photo is read once. The runtime hands over a file reference, and the hook turns it into a data URL and releases it, so there is nothing to clean up.
useGeoLocation
useGeoLocation reads the current position through the native runtime's geolocation plugin, and through navigator.geolocation in a browser. It sends the user to the app settings when the permission is denied.useGeoLocation()ReturnsDescription
getPosition({ enableHighAccuracy })
Returns a
Position. When the permission is denied, opens the settings and returns undefined.checkPermission()
Requests permission and returns
{ location, precise }.An app hook that finds where to center a map:
apps/myapp/webkit/useMapCenter.tsx
- Check for
undefined. It means the permission was denied and the settings screen is already open. - The position is flat.
latitude,longitude,accuracy,altitude,heading,speedandtimestampsit on the value itself, not undercoords. precisesays whether the fix is exact.falsemeans the user granted approximate location only; the web answersnull.- Declare the permission.
"location"innative.permissionsadds the geolocation plugin and its usage texts.
usePushNotification
Push lives in
@libs/util/webkit, not akanjs/webkit. In a native shell the hook calls the runtime's push plugin (APNs on iOS, FCM on Android); in a browser it calls Firebase. Either way it hands back one PushToken shape.import { type PushToken, usePushNotification } from "@libs/util/webkit";ReturnsDescription
register()
Asks for permission, then returns a
PushToken, or undefined when refused or unsupported.getToken()
Returns the token without asking. Registering shows no prompt, so check the permission first.
getPermission()requestPermission()
Read the permission state, or show the prompt and return the answer.
isSupported()
Tells whether push can work in this runtime.
onTokenChange(listener)
Hands each token a native shell rotates to the listener. Returns the unsubscribe.
initClickBridge()
Routes the browser's notification clicks. The hook runs it on mount; native taps need nothing.
Registering from a button, because
register() may show a permission prompt:apps/myapp/ui/EnablePush.tsx
- A
PushTokenis the address of one install. It holdstoken,platform(web|ios|android),provider(apns|fcm) anddeviceId, the installation id kept in the app's storage. - A tap opens the push's
url. In a native shell the framework routes it from boot, the launching tap included; only a path inside the app is followed. registerPushTokencomes withlibs/shared. Its notification store keeps the token on the signed-in user;Notification.Zone.Initializekeeps it current.
usePurchase
usePurchase sells in-app products through the native runtime's iap plugin: StoreKit 2 on iOS, Play Billing on Android. Your server verifies every transaction before the app credits it, and a browser sells nothing.import { usePurchase } from "akanjs/webkit/usePurchase";Options
platform"ios" | "android" | "all"required
The stores the app sells in. A native shell on another platform shows no products.
productInfo{ id, type: "consumable" | "nonConsumable" | "subscription" }[]required
The store product ids and what each one is. A product not listed is finished without being consumed.
urlstringrequired
The verification server's origin. It answers
POST <url>/billing/verifyBilling.onPay(transaction, verified) => void | Promise<void>
Credits a consumable or non-consumable.
verified is the server's JSON answer.onSubscribe(transaction, verified) => void | Promise<void>
The same, for a subscription.
ReturnsDescription
products
The store's
IapProducts for productInfo: title, displayPrice, price, currency and offers.isLoading
true until the products and the unfinished transactions are loaded.purchaseProduct(product, offerToken?)
Opens the store sheet and answers
"purchased", "pending", "cancelled" or "unverified".restorePurchases()
Returns what the person owns now, finishing an Android purchase still unacknowledged on the way.
A buy button that credits coins once the server has accepted the purchase:
apps/myapp/ui/BuyCoins.tsx
- Add the plugin to the native app. The iap plugin is not a permission: name it with
native: { plugins: ["iap"] }inakan.config.ts. - The server gets one body:
{ data }.datais{ platform, packageName, productId, receipt, transactionId }. On iOSreceiptis the transaction's signed JWS (no app receipt, no account id), on Android the purchase token with its package name. Any 2xx accepts it, and its JSON reachesonPayoronSubscribeasverified. - Finished only once it is credited. A transaction is finished after the server accepted it and your callback resolved. A refusal or a throw leaves it unfinished, and the store hands it over again: iOS at the next launch, while Google refunds an unacknowledged purchase after three days.
- Leftovers settle at mount. The hook loads the unfinished transactions and listens for later ones (Ask to Buy, a pending payment), and one transaction is credited once even when it arrives twice.
useLocation / useHistory
The CSR router runs on these two hooks: one turns an href into route state, the other remembers where the user has been. They drive cached page transitions, scroll restoration and back/forward detection.
APIDescription
useLocation({ rootRouteGuide })
Returns
getLocation(href), which matches an href against the route tree.getLocation(href)
Gives
pathname, search, hash, params, searchParams and the matched pathRoute.useHistory(locations)
Keeps visited locations, the current index and each page's scroll position in a ref.
setHistoryForwardsetHistoryBack
Record a push, replace or pop, saving the scroll of the page being left.
getPrevLocationgetCurrentLocationgetNextLocation
Read the entries around the current one. Back and forward are told apart with them.
getScrollTop(location)
The scroll position to restore: the saved one, or the
#hash element's top.The router sets them up once, starting from the page it opened on:
pkgs/akanjs/webkit/useCsrValues.ts
- App code navigates with
router.router.pushandrouter.backfromakanjs/clientgo through these hooks for you. - A page with
cachein its config is kept. The history remembers its location, so going back shows it without rebuilding it.
LoginForm
LoginForm is what the shared store's login action takes. It says which account to load after sign-in, and where to send the user on success or failure.auth"user" | "admin" | "public"required
"admin" loads the admin account. Any other value loads the user with getSelf.redirectstring
Where
router.push goes after the account loads.unauthorizestring
Where to go when loading the account fails.
jwtstring | null
A token saved with
setAuth before anything loads, for example right after sign-in.A button that loads the signed-in user, then lands on the home page or goes back to sign-in:
apps/myapp/ui/Continue.tsx
- Pass
jwtright after a sign-in call. The admin store does this: it signs in, then callsloginwith{ auth: "admin", jwt, redirect }. "public"loads the same way as"user"today. Only"admin"takes a different path.