@askrjs/askr
v0.0.28
Published
Actor-backed deterministic UI framework
Readme
Askr
Deterministic UI runtime with runtime enforcement.
Quick Start
import { createIsland, state } from '@askrjs/askr';
function Counter() {
const [count, setCount] = state(0);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
}
createIsland({ root: document.body, component: Counter });Core Features
Runtime Enforcement
Askr validates component structure as it runs, catching mistakes with clear error messages.
// This error is caught immediately:
if (condition) {
const [x, setX] = state(0); // ❌ Hook order violation
}
// Error shows the fix:
const [x, setX] = state(0);
if (condition) {
setX(newValue); // ✅ Correct
}Deterministic Execution
Events serialize through a scheduler. State updates are atomic. Renders follow strict ordering.
// Event 1 completes (handler + state + DOM)
// Then Event 2 starts
// No race conditionsProven with 524 tests covering:
- Event ordering (12 tests)
- State atomicity (12 tests)
- Transaction semantics (30 tests)
Automatic Cleanup
Every component gets an AbortSignal for automatic cancellation.
import { resource } from '@askrjs/askr/resources';
function Data({ id }) {
const data = resource(async ({ signal }) => {
const res = await fetch(`/api/${id}`, { signal });
return res.json();
}, [id]);
if (data.pending || !data.value) return <div>Loading...</div>;
if (data.error) return <div>Failed to load</div>;
return <div>{data.value.name}</div>;
}
// Async work is cancelled automatically on unmount/navigationExplicit Reactivity
Getters and setters are functions. Call the getter to read and the setter to update — this makes reactivity explicit in your code.
const [count, setCount] = state(0);
console.log(count()); // getter: read
setCount(1); // setter: writeClear data flow. No hidden subscriptions.
API
State
const [value, setValue] = state(initialValue);
// Read
value();
// Write
setValue(newValue);
// Update
setValue((prev) => prev + 1);Derived State
function Counter() {
const [count, setCount] = state(0);
const doubled = derive(() => count() * 2);
return (
<button onClick={() => setCount((prev) => prev + 1)}>
{count()} -> {doubled()}
</button>
);
}derive() now returns a getter. Migrate const doubled = derive(...); {doubled} to const doubled = derive(...); {doubled()}.
Keyed Selectors
function Table({ rows }) {
const [selectedId, setSelectedId] = state<number | null>(null);
const isSelected = selector(selectedId);
return For(
() => rows(),
(row) => row.id,
(row) => (
<tr class={() => (isSelected(row.id) ? 'danger' : '')}>
<td>
<a onClick={() => setSelectedId(row.id)}>{row.id}</a>
</td>
</tr>
)
);
}Use selector() for row selection, active-route checks, and similar keyed fanout hotspots. Create it once in the owner component and reuse the keyed predicate across rows.
Lists
const [items, setItems] = state([...]);
For(
items,
(item) => item.id,
(item) => <Item {...item} />
)Apps
// Single component
createIsland({
root: document.body,
component: MyComponent,
});
// Routed app
route('/', () => <Home />);
route('/about', () => <About />);
createSPA({
root: document.body,
routes: getRoutes(),
});Documentation
- Documentation Index
- Install
- Quick Start
- State Management
- Router Guide
- Resources Guide
- SSG Guide (Advanced)
- Runtime Enforcement
- Deterministic Execution
- API Reference
Guarantees
Askr provides provable guarantees, tested with 524 tests:
- Hook order enforcement (12 tests)
- Event serialization (12 tests)
- Atomic transactions (30 tests)
- Keyed reconciliation (12 tests)
- Memory safety (8 tests)
Migration
Coming from React
| React | Askr |
| ------------------------------- | ---------------------------- |
| const [x, setX] = useState(0) | const [x, setX] = state(0) |
| x | x() |
| setX(1) | setX(1) |
The main difference: values are functions that you call to read.
Install
npm install @askrjs/askrLicense
Apache 2.0
