A high-performance framework with fine-grained observable-based reactivity for rich client apps.
- No VDOM: direct DOM updates, no diffing
- No stale closures: functions always run fresh
- No hooks rules: hooks are plain functions, call anywhere
- No dependency arrays: automatic tracking
- No key prop: map arrays or use
Forwith unique values - Local-first: no SSR/hydration/streaming (by design)
import { $, get } from 'voby';
const count = $(0); // create observable
count(); // read: 0
count(1); // write: 1
count(v => v + 1); // update: 2
get(count); // unwrap: 2 (works on any value)import { $, useEffect, useMemo } from 'voby';
const a = $(1);
const b = $(2);
const sum = useMemo(() => a() + b());
useEffect(() => {
console.log(sum());
});const Counter = () => {
const count = $(0);
return (
<button onClick={() => count(v => v + 1)}>
Count: {count}
</button>
);
};import { $, render } from 'voby';
const App = () => {
const count = $(0);
return (
<button onClick={() => count(v => v + 1)}>
Count: {count}
</button>
);
};
render(<App />, document.getElementById('app')!);import { lazy, render } from 'voby';
import { Router, Routes, Route, A, Outlet, Navigate } from 'voby/router';
import { useParams, useNavigate, useLocation, useSearchParams, useRouteData } from 'voby/router';
const Home = lazy(() => import('./pages/Home'));
const Users = lazy(() => import('./pages/Users'));
const User = lazy(() => import('./pages/User'));
render(
<Router>
<nav>
<A href="/">Home</A>
<A href="/users" end>Users</A>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/users" element={<Users />} />
<Route path="/users/:id" element={<User />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</Router>,
document.getElementById('app')!
);
const DashboardLayout = () => (
<div class="dashboard">
<Sidebar />
<Outlet />
</div>
);
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="/" element={<Overview />} />
<Route path="/settings" element={<Settings />} />
</Route>
const UserPage = () => {
const params = useParams();
const user = useResource(() => fetchUser(params.id));
return <div>{user().value?.name}</div>;
};
const navigate = useNavigate();
navigate('/users/123');
const location = useLocation();
const [searchParams, setSearchParams] = useSearchParams();
setSearchParams({ page: 2, sort: 'name' });
<Route path="/old" element={<Navigate href="/new" />} />See docs/router.md for full router docs and edge cases.
import { useResource, Suspense, lazy } from 'voby';
const [resource] = useResource(() => fetch('/api').then(r => r.json()));
const LazyComponent = lazy(() => import('./Component'));
<Suspense fallback={<Spinner />}>
<LazyComponent />
</Suspense>import { store } from 'voby';
const state = store({ user: { name: 'John' }, items: [] });
state.user.name = 'Jane';
state.items.push('item');- Use
classinstead ofclassName classaccepts string, object{active: bool}, or array- Attribute values can be observables or functions (fine-grained updates)
refmust be function form:ref={el => ...}orref={[fn1, fn2]}- Refs called on next microtask (node likely attached to DOM)
- No
keyprop needed stylenumbers auto-suffixed withpx, CSS vars supporteddangerouslySetInnerHTMLsupported (not innerHTML)- Delegated events:
click,input,keydown,keyup,mousedown,mouseup,dblclick,focusin,focusout,beforeinput
| API | Notes |
|---|---|
$ |
create observable |
get |
unwrap observable/function |
batch |
batch updates |
createContext |
context factory |
createElement |
JSX factory |
h |
hyperscript helper |
hmr |
hot module helper |
html |
tagged template JSX alternative |
isBatching |
batching state |
isObservable |
observable check |
isServer |
server env check |
isStore |
store check |
lazy |
code-split component |
mergeProps |
merge props (composition) |
mergePropsN |
merge props (N inputs) |
mergeRefs |
merge refs |
render |
mount app |
renderElement |
base UI composition |
renderToString |
client-side string render |
resolve |
normalize functions to memos |
store |
deep reactive store |
template |
static template optimization |
tick |
flush effects |
untrack |
read without tracking |
| API | Notes |
|---|---|
Dynamic |
dynamic component/element |
ErrorBoundary |
error isolation |
For |
keyed list rendering |
Fragment |
JSX fragment |
If |
reactive conditional |
KeepAlive |
component cache |
Portal |
render elsewhere |
Suspense |
async boundary |
| API | Notes |
|---|---|
useAbortController |
AbortController helper |
useAbortSignal |
AbortSignal helper |
useAnimationFrame |
RAF scheduler |
useAnimationLoop |
RAF loop |
useBoolean |
boolean observable |
useCleanup |
cleanup on dispose |
useDisposed |
reactive disposed flag |
useEffect |
reactive effect |
useEventListener |
DOM event helper |
useFetch |
fetch wrapped as resource |
useIdleCallback |
idle callback |
useIdleLoop |
idle loop |
useInterval |
interval helper |
useMemo |
derived observable |
useMicrotask |
microtask helper |
usePromise |
promise resource |
useReadonly |
readonly view |
useResolved |
unwrap values/tuples |
useResource |
async resource with mutate/refetch |
useRoot |
isolated root |
useSelector |
optimized selector |
useSuspended |
Suspense state |
useTimeout |
timeout helper |
useUntracked |
return untracked function |
| API | Notes |
|---|---|
EffectOptions |
effect options |
FunctionMaybe |
value or function |
MemoOptions |
memo options |
Observable |
writable observable |
ObservableLike |
observable-like |
ObservableReadonly |
readonly observable |
ObservableReadonlyLike |
readonly observable-like |
ObservableMaybe |
observable or value |
ObservableOptions |
observable options |
Resource |
resource shape |
StoreOptions |
store options |
JSX |
JSX types (from runtime) |
| API | Notes |
|---|---|
jsx / jsxs / jsxDEV |
JSX runtime exports |
Fragment |
JSX fragment |
A Solid-style resource with mutate/refetch, tracked source, and Suspense integration.
const [todos, { mutate, refetch }] = useResource(getTodos);
mutate(prev => (prev ? prev.concat(newTodo) : [newTodo]));
refetch();mutateupdates value without fetchingrefetchre-runs fetcher (optionally with custom info)- pending state can trigger
Suspense
Best practice: when using a source, let it reflect real state. If it can be
null/undefined, the fetcher should handle that case explicitly.
const [todos] = useResource(
() => user(),
(u) => (u ? fetchTodos(u.id) : []),
);Common mistake: relying on implicit gating. useResource always re-runs the
fetcher when the source changes; if your source can be empty, handle it in the
fetcher.
See docs/resource.md for behavior details.
Context should be initialized per Provider. Prefer factory form:
const [AppProvider, useApp] = createContext((props: { initial?: number }) => ({
count: $(props.initial ?? 0),
}));If you need to inject a value, use the explicit value prop:
const [AppProvider, useApp] = createContext<{ theme: Observable<string> }>();
// <AppProvider value={{ theme: $("light") }}>{...}Common mistake: creating state outside and passing it as createContext(state).
That runs immediately at module load and defeats per-Provider initialization.
Provider wraps children with a built-in error boundary; if init or render throws, it renders the error message instead of children.
Base UI composition with render-prop support and merged props/handlers.
import { renderElement, $ } from 'voby';
const DialogTrigger = (props) => {
const open = $(false);
return renderElement('button', props, {
state: { open },
props: {
onClick: () => open(v => !v),
'data-open': () => (open() ? '' : undefined)
}
});
};Local-only Suspense. No SSR/hydration. Use for async boundaries and lazy components.
docs/index.mddocs/api-reference.mddocs/hooks.mddocs/resource.mddocs/router.md