@llui/effects
@llui/effects
Effect builders for LLui. Effects are data -- update() returns them, the runtime dispatches.
pnpm add @llui/effects
Usage
import { http, cancel, debounce, handleEffects } from '@llui/effects'
// Debounced search with cancel
function update(state: State, msg: Msg): [State, Effect[]] {
switch (msg.type) {
case 'search':
return [
{ ...state, query: msg.value },
[
cancel('search'),
debounce(
'search',
300,
http({
url: `/api/search?q=${msg.value}`,
onSuccess: (data) => ({ type: 'results', data }),
onError: (err) => ({ type: 'searchError', err }),
}),
),
],
]
}
}
// Wire up in component
const handler = handleEffects<Effect, Msg>()
.use(httpPlugin)
.else((effect, send) => {
/* custom effects */
})
API
Effect Builders
| Function | Description |
|---|---|
http({ url, onSuccess, onError }) |
HTTP request effect |
cancel(token, inner?) |
Cancel by token, optionally replace with inner |
debounce(key, ms, inner) |
Debounce inner effect by key |
timeout(ms, msg) |
Fire msg after delay |
interval(ms, msg) |
Fire msg on interval |
storageSet(key, value, storage?) |
Write to localStorage/sessionStorage |
storageGet(key, onResult, storage?) |
Read from storage |
storageRemove(key, storage?) |
Remove from storage |
storageWatch(key, onChange) |
Watch storage for changes |
broadcast(channel, data) |
Send on BroadcastChannel |
broadcastListen(channel, onMsg) |
Listen on BroadcastChannel |
sequence([...effects]) |
Run effects in order |
race([...effects]) |
Run effects concurrently, first wins |
upload({ url, body, onProgress, onSuccess, onError }) |
File upload with progress via XHR |
clipboardRead({ onSuccess, onError }) |
Read text from clipboard |
clipboardWrite(text) |
Write text to clipboard (fire-and-forget) |
notification(title, opts?) |
Show browser notification (requests permission) |
geolocation({ onSuccess, onError, enableHighAccuracy? }) |
One-shot geolocation position |
Upload
Upload files with progress tracking via XMLHttpRequest:
import { upload } from '@llui/effects'
const effect = upload({
url: '/api/upload',
body: formData,
headers: { Authorization: `Bearer ${token}` },
onProgress: (loaded, total) => ({
type: 'uploadProgress',
pct: Math.round((loaded / total) * 100),
}),
onSuccess: (data, status) => ({ type: 'uploadDone', data, status }),
onError: (error) => ({ type: 'uploadFailed', error }),
})
Clipboard
Read and write text via the Clipboard API:
import { clipboardRead, clipboardWrite } from '@llui/effects'
// Copy text to clipboard (fire-and-forget)
clipboardWrite('Hello, world!')
// Read text from clipboard
clipboardRead({
onSuccess: (text) => ({ type: 'pasted', text }),
onError: (error) => ({ type: 'clipError', error }),
})
Notification
Show browser notifications (requests permission automatically):
import { notification } from '@llui/effects'
notification('New message', {
body: 'You have a new message from Alice',
icon: '/avatar.png',
onClick: () => ({ type: 'openChat' }),
onError: () => ({ type: 'notifBlocked' }),
})
Geolocation
One-shot position request:
import { geolocation } from '@llui/effects'
geolocation({
enableHighAccuracy: true,
onSuccess: (pos) => ({
type: 'located',
lat: pos.latitude,
lng: pos.longitude,
}),
onError: (error) => ({ type: 'geoError', error }),
})
Effect Handling
| Function | Description |
|---|---|
handleEffects<E, M>() |
Chainable effect handler builder |
.use(plugin) |
Add an effect handler plugin |
.else(handler) |
Fallback for unhandled effects |
resolveEffects(def) |
SSR data loading -- resolves effects server-side |
Types
| Type | Description |
|---|---|
Async<T, E> |
idle | loading | success | failure -- async data state |
ApiError |
network | timeout | notfound | unauthorized | forbidden | ratelimit | validation | server |
License
MIT