State management with request isolation, async action state and versioned persistence built in. Framework-agnostic, with no runtime dependencies.
- Typed stores without generics.
defineStoreinfers state, getters and actions;thisinside an action is the fully typed store. - Deep reactivity. Mutate state directly. Reads inside effects, computed values and React selectors are tracked per property.
- Async actions that report their own state. Every action has reactive
pendinganderror, anabort(), and anAbortSignalatthis.$signal. - Request-scoped containers. One container per app, per server request or per test, with
dehydrate()/hydrate()for SSR. - Persistence with migrations. Versioned schemas, cross-tab sync, and sanitised loading of untrusted storage.
| Package | |
|---|---|
@quantajs/core |
Reactivity, stores, containers, persistence |
@quantajs/react |
React hooks and provider |
@quantajs/devtools |
In-page state inspector (optional) |
npm install @quantajs/core @quantajs/react// stores/cart.ts
import { defineStore } from '@quantajs/core';
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] as { name: string; price: number }[] }),
getters: {
total: (s) => s.items.reduce((sum, item) => sum + item.price, 0),
},
actions: {
add(name: string, price: number) {
this.items.push({ name, price });
},
async checkout() {
await fetch('/api/checkout', {
method: 'POST',
signal: this.$signal,
});
this.items = [];
},
},
});// Cart.tsx
import { useQuantaActions, useQuantaValue } from '@quantajs/react';
import { useCartStore } from './stores/cart';
export function Cart() {
const total = useQuantaValue(useCartStore, (s) => s.total);
const pending = useQuantaValue(useCartStore, (s) => s.checkout.pending);
const error = useQuantaValue(useCartStore, (s) => s.checkout.error);
const cart = useQuantaActions(useCartStore);
return (
<>
<button
disabled={pending}
onClick={() => cart.checkout().catch(() => {})}
>
Pay ${total}
</button>
{error && <p>{error.message}</p>}
</>
);
}useQuantaValue re-renders only when what the selector read changes. useQuantaActions never re-renders.
The same store works without a framework:
// main.ts
import { useCartStore } from './stores/cart';
const cart = useCartStore();
cart.subscribe(() => console.log('total', cart.total));
cart.add('Widget', 9.99);A store definition holds no state, so it is safe at module scope. On the server, resolve it against a container created per request:
// app/page.tsx — a Server Component
import { createContainer } from '@quantajs/core';
import { useCartStore } from '../stores/cart';
import { Cart } from '../Cart';
import { Providers } from './providers';
export default function Page() {
const container = createContainer();
useCartStore(container).add('Widget', 9.99);
const snapshot = container.dehydrate();
container.dispose();
return (
<Providers snapshot={snapshot}>
<Cart />
</Providers>
);
}// app/providers.tsx
'use client';
import type { ReactNode } from 'react';
import type { ContainerSnapshot } from '@quantajs/core';
import { QuantaProvider } from '@quantajs/react';
export function Providers(props: {
snapshot: ContainerSnapshot;
children: ReactNode;
}) {
return (
<QuantaProvider snapshot={props.snapshot}>
{props.children}
</QuantaProvider>
);
}See examples/nextjs-app for the complete App Router setup.
examples/vanilla— no framework, with persistenceexamples/react-vite— every React hook, async actions, DevToolsexamples/nextjs-app— per-request containers and hydration
Each is built and verified in CI.
See CONTRIBUTING.md.
