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▾
UI 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
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
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". Put lazy() in the ui/<Folder>/index_.tsx boundary file.
  • Push and speech come from the util library. usePushNotification and useSpeech are 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
CallServer HTML
↳ While the chunk loads
No optionThe component itself.
The nearest boundary, usually the whole route, shows its fallback.
suspense: trueThe component, sent after the shell in stream mode.
Only this spot shows loading.
ssr: falseOnly 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" and lazy() go in index_.tsx; the index.tsx beside 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: true only 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 stale shopId.
  • 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 delay restarts it.
  • Ticks do not wait for each other. An async callback that takes longer than delay overlaps 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.
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, while useFetch waits 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.storage is missing, so a browser-only call needs the ssr: false boundary shown above.
  • It follows the promise handed on each render. A new promise, or new deps on useFetchFn, resets the result to fulfilled: false until it resolves, and a result from an older promise is dropped.

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 } = {})
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" in native.permissions adds 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()
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, speed and timestamp sit on the value itself, not under coords.
  • precise says whether the fix is exact. false means the user granted approximate location only; the web answers null.
  • Declare the permission. "location" in native.permissions adds 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";
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 PushToken is the address of one install. It holds token, platform (web | ios | android), provider (apns | fcm) and deviceId, 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.
  • registerPushToken comes with libs/shared. Its notification store keeps the token on the signed-in user; Notification.Zone.Initialize keeps 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.
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"] } in akan.config.ts.
  • The server gets one body: { data }. data is { platform, packageName, productId, receipt, transactionId }. On iOS receipt is 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 reaches onPay or onSubscribe as verified.
  • 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.
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.push and router.back from akanjs/client go through these hooks for you.
  • A page with cache in 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 jwt right after a sign-in call. The admin store does this: it signs in, then calls login with { auth: "admin", jwt, redirect }.
  • "public" loads the same way as "user" today. Only "admin" takes a different path.

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