lor-cui-kit
v0.1.1
Published
Runtime utilities for lazy CUI card activation, layout stabilization, persistent previous-data hydration, and stale-while-revalidate behavior in React applications.
Maintainers
Readme
lor-cui-kit
English | 简体中文
Runtime kit for lazy CUI card activation, layout-stable height reservation, persistent previous-data hydration, and stale-while-revalidate in React chat / agent history views.
Why you need it
Agent and IM history often mix plain text with rich CUI cards (orders, tickets, tools). A naive list mounts every card and fetches immediately. Fifty messages with ten cards means ten network calls on first paint, layout jumps when heights resolve, and wasted work for cards far below the viewport.
lor-cui-kit keeps the list cheap: shells reserve height, children mount only near the viewport, and a previous payload can hydrate from storage while a background refresh runs.
Core capabilities
- Lazy activation —
CuiCardShellobserves the viewport (preloadMargindefault600px 0px) and mounts children only when intersecting. Passlazy={false}to skip this when the host already lazy-loads. - Height reservation — inactive shells use cached height or
defaultHeight(240) so scrolling does not collapse. - Previous-data hydration —
useCuiResourcecan show the last persisted payload instantly, then revalidate. - TTL + stale-while-revalidate —
ttl/staleTtlmap to fresh / stale / expired; default mount policy isif-stale. - Scoped isolation —
namespace+scopeKeypartition both height and resource persistence (account / tenant / env). - Targeted invalidation — exact key, prefix (
ordermatchesorderandorder:…), or fullclearScope(). - SWR integration — wrap with
SWRConfig+CuiKitProvider; invalidation also mutates the surrounding SWR cache.
Run the in-repo comparison: pnpm demo.
Install
pnpm add lor-cui-kit
pnpm add react swrPeers: react ^18.2.0 || ^19.0.0, swr ^2.0.0. Engine: Node >=20.19.0. The package is ESM-only.
import { createCuiKit, CuiKitProvider, CuiCardShell, useCuiResource } from 'lor-cui-kit'
import { useCuiKit } from 'lor-cui-kit/react'
import { createMemoryStorageAdapter } from 'lor-cui-kit/core'Quick start
1. Create CuiKit
const cuiKit = createCuiKit({
namespace: 'agent-chat',
scopeKey: `${environment}:${tenantId}:${userId}`,
persistResources: true,
})2. Provider
Give SWR an isolated Map per kit/session so invalidate does not leak across users.
<SWRConfig value={{ provider: () => new Map() }}>
<CuiKitProvider value={cuiKit}>
<Chat />
</CuiKitProvider>
</SWRConfig>3. CuiCardShell
<CuiCardShell cardKey={message.id}>
<OrderCard orderId={orderId} />
</CuiCardShell>Inactive shells render no children. Put useCuiResource inside the card, not around the shell, or every card will fetch.
4. useCuiResource
const { data, error, isLoading, isValidating, isStale } = useCuiResource({
key: `order:${orderId}`,
fetcher: () => getOrder(orderId),
ttl: 30_000,
staleTtl: 5 * 60_000,
persist: true,
})key: null disables the request (isLoading is false). Pass the same CuiKit instance to CuiKitProvider for the lifetime of that scopeKey.
TTL / staleTtl semantics
Each write stores expiresAt = now + ttl. Reads classify the entry:
| State | When | UI / fetch (default revalidateOnMount: 'if-stale') |
| --- | --- | --- |
| fresh | now < expiresAt | Show data. No fetch on mount. |
| stale | expiresAt <= now < expiresAt + staleTtl | Show previous data. Background fetch. isStale === true. |
| expired | now >= expiresAt + staleTtl | Entry is deleted. Treated as a miss; fetch if the hook is mounted. |
| miss | no entry | Fetch. |
staleTtl defaults to the kit’s defaultStaleTtl (300_000 ms). Override per hook.
revalidateOnMount:
'if-stale'(default) — fetch on miss / stale / expired, not on fresh.'always'— always fetch on mount.false— never fetch on mount (manualrevalidate()).
Failed late writes (generation changed by invalidate) are dropped; the hook keeps the latest cache payload.
Invalidation after writes / prefix invalidate
After a mutation, drop cache and tell SWR to refetch mounted hooks:
import { invalidateCuiResource, invalidateCuiPrefix, clearCuiScope } from 'lor-cui-kit'
await invalidateCuiResource(cuiKit, `order:${orderId}`)
await invalidateCuiPrefix(cuiKit, 'order')
await clearCuiScope(cuiKit)Prefix match is key === prefix or key.startsWith(prefix + ':'). order matches order and order:42, not order-archive.
CuiKitProvider binds useSWRConfig().mutate so these helpers also update in-memory SWR keys. clearCuiScope clears height + resource persistence for the current namespace/scope and mutates matching SWR keys with { revalidate: false }. Call it on logout.
Height cache and layoutKey
CuiCardShell keys a height as (cardKey, layoutKey):
- Omit
layoutKeyand the shell buckets content width intow100,w200, … (100pxbuckets) so desktop vs phone do not share one height. - Pass
layoutKeyyourself when width is not the right axis (for example'compact'vs'expanded').
Behavior:
- Inactive — CSS
height= cached value ordefaultHeight. - Active, not yet measured —
minHeightuses the same reservation. - Measured — forced height is removed; a
ResizeObserveron the inner content writes the cache.
Heights persist through the height storage adapter (localStorage by default), independently of persistResources.
scopeKey and account / tenant isolation
namespace is the product surface (agent-chat). scopeKey is the privacy and cache boundary. Recommended:
scopeKey: `${environment}:${tenantId}:${userId}`Storage keys look like lor-cui-kit:v1:height:… and lor-cui-kit:v1:resource:…, namespaced with URI-encoded namespace and scopeKey. Two users on one browser profile do not share entries if scopeKey differs.
On account switch or logout:
await clearCuiScope(previousKit)Then create a new CuiKit (and a new SWR Map) for the next scopeKey. Do not reuse a kit across users.
localStorage privacy warning
Default adapters write to window.localStorage in plaintext.
- Heights are persisted even when
persistResourcesisfalse. - Business payloads are persisted only when
persistResources: trueor the hook setspersist: true. - Keys include
namespace,scopeKey,cardKey, and resource keys (for exampleorder:42). Values are JSON of your fetcher result. - Anyone with device access, another script on the origin, or XSS can read this cache. Do not persist secrets, tokens, or regulated PII unless that risk is accepted.
- Shared / kiosk browsers: always
clearCuiScopeon logout. For tests or no persistence, passcreateMemoryStorageAdapter()asheightStorage/resourceStorage. - On the server there is no
window; the localStorage adapter falls back to memory anduseCuiResourceforcespersist: false.
SSR limitations
This kit is client-first. React entries are marked 'use client'.
- Server
CuiCardShellalways usesdefaultHeight(240). Cached heights are not read duringrenderToString(useSyncExternalStoreserver snapshot isundefined). Withlazy(default), children are not rendered until the client observer activates the shell. Withlazy={false}, children render immediately, including during SSR. persistisfalsewhentypeof window === 'undefined'.- No
IntersectionObserver→ the shell activates after the ref attaches. - No
ResizeObserver→ height is measured once viagetBoundingClientRect. - Hydration: expect the reserved
240px(or yourdefaultHeight) on the server HTML; the client may replace it with a cached height after paint. Avoid puttinguseCuiResourcein a Server Component.
API Reference
Package exports
| Entry | Runtime |
| --- | --- |
| lor-cui-kit | createCuiKit, CuiKitProvider, CuiCardShell, useCuiResource, invalidateCuiResource, invalidateCuiPrefix, clearCuiScope, createLocalStorageAdapter, createMemoryStorageAdapter |
| lor-cui-kit/react | Above React APIs plus useCuiKit, useCuiVisibility |
| lor-cui-kit/core | createCuiKit, invalidation helpers, storage adapters |
createCuiKit(options)
| Option | Default | Notes |
| --- | --- | --- |
| namespace | required | Product / feature id. |
| scopeKey | required | Account / tenant / env isolation. |
| defaultHeight | 240 | Reserved height before measure. |
| preloadMargin | '600px 0px' | IntersectionObserver rootMargin. |
| defaultStaleTtl | 300_000 | Fallback staleTtl for reads. |
| persistResources | false | Default for useCuiResource({ persist }). |
| heightStorage | localStorage adapter | Implements StorageAdapter. |
| resourceStorage | localStorage adapter | L2 write only when persist is true. |
| now | Date.now | Inject for tests. |
| onStorageError | — | Called when localStorage throws. |
effectivePersist(persist?) is persist ?? persistResources. Providing resourceStorage does not by itself persist; the hook/persistResources flag must be true.
CuiCardShell
| Prop | Default | Notes |
| --- | --- | --- |
| cardKey | required | Stable id (message id). |
| layoutKey | auto w{bucket} | Width bucket of 100px, or your own key. |
| keepMounted | true | Once activated, stay mounted when leaving the viewport. Ignored when lazy is false. |
| lazy | true | Observe the viewport and mount children only when intersecting. Set false if the host already lazy-loads cards. |
| className | — | Applied to the outer shell. |
| children | required | Mounted only while active, or immediately when lazy is false. |
useCuiResource(options)
Must run under CuiKitProvider.
| Option | Default | Notes |
| --- | --- | --- |
| key | required | Logical key, or null to pause. |
| fetcher | required | () => Promise<T>. |
| ttl | required | Fresh window in ms. |
| staleTtl | kit defaultStaleTtl | Stale window after ttl. |
| persist | kit persistResources | Forced false without window. |
| revalidateOnMount | 'if-stale' | 'always' | 'if-stale' | false. |
| dedupeInterval | 2000 | SWR dedupingInterval. |
| keepPreviousData | true | SWR option. |
Result: data, error, isLoading, isValidating, isStale, updatedAt, revalidate().
Invalidation helpers
invalidateCuiResource(kit, key)
invalidateCuiPrefix(kit, prefix)
clearCuiScope(kit)Each takes an explicit CuiKit. There is no global singleton.
StorageAdapter
interface StorageAdapter {
getItem(key: string): string | null
setItem(key: string, value: string): void
removeItem(key: string): void
keys(): Iterable<string>
}License: MIT.
