Quick Start
This guide gets you from install to a working app with @quantajs/core,
@quantajs/react, and optional @quantajs/devtools.
Coming from 2.0.0? See Migrating to 2.1.
1. Define a store
defineStore returns a definition — a blueprint you call to get the
instance. It holds no state itself, which is what makes it safe at module scope
even on a server.
// stores/counter.ts
import { defineStore } from '@quantajs/core';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubled: (s) => s.count * 2,
},
actions: {
increment() {
this.count++;
},
decrement() {
this.count--;
},
},
});
Everything is inferred — you never restate a generic:
const counter = useCounterStore();
counter.count; // number
counter.doubled; // number
counter.increment(); // typed
2. Use it in React
import { useQuanta } from '@quantajs/react';
import { useCounterStore } from './stores/counter';
function Counter() {
const counter = useQuanta(useCounterStore);
return (
<div>
<p>Count: {counter.count}</p>
<p>Doubled: {counter.doubled}</p>
<button onClick={() => counter.increment()}>+</button>
<button onClick={() => counter.decrement()}>-</button>
</div>
);
}
export default function App() {
return <Counter />;
}
No provider is required in a client-only app — stores resolve against an ambient container. Add one when you need to control the container's lifetime, which is mostly under SSR:
import { QuantaProvider } from '@quantajs/react';
<QuantaProvider>
<Counter />
</QuantaProvider>
3. Subscribe to less
useQuanta subscribes to the whole store. In a view that renders a lot, use a
selector instead — it subscribes to exactly what the selector reads, so a
component reading s.count is not woken by a write to s.name.
import { useQuantaValue, useQuantaActions } from '@quantajs/react';
/** Re-renders only when `count` changes. */
function CountLabel() {
const count = useQuantaValue(useCounterStore, (s) => s.count);
return <p>{count}</p>;
}
/** Never re-renders — it calls an action but reads no state. */
function IncrementButton() {
const counter = useQuantaActions(useCounterStore);
return <button onClick={() => counter.increment()}>+</button>;
}
4. Async, without the boilerplate
Every action carries pending, error and abort(). You do not write loading
flags by hand.
export const useUsersStore = defineStore('users', {
state: () => ({ list: [] as User[] }),
actions: {
async load() {
const res = await fetch('/api/users', { signal: this.$signal });
this.list = await res.json();
},
},
});
function Users() {
const users = useQuanta(useUsersStore);
return (
<>
<button onClick={() => users.load().catch(() => {})} disabled={users.load.pending}>
{users.load.pending ? 'Loading…' : 'Load users'}
</button>
{users.load.error && <p role="alert">{users.load.error.message}</p>}
<ul>{users.list.map((u) => <li key={u.id}>{u.name}</li>)}</ul>
</>
);
}
See Async Actions.
5. Persist state (optional)
import { defineStore, LocalStorageAdapter } from '@quantajs/core';
export const useSettingsStore = defineStore('settings', {
state: () => ({ theme: 'light', language: 'en' }),
actions: {
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
},
},
persist: {
adapter: new LocalStorageAdapter('settings'),
debounceMs: 300,
},
});
// Wait for restored state if you need it before rendering.
await useSettingsStore().$hydrated;
See Persistence.
6. DevTools (optional)
DevTools is opt-in as of 2.1 — it never attaches to window until you ask
it to.
import { enableDevTools } from '@quantajs/core';
if (import.meta.env.DEV) {
enableDevTools();
}
import { mountDevTools } from '@quantajs/devtools';
mountDevTools();
For React apps you can render <QuantaDevTools /> from @quantajs/react/devtools
instead of calling mountDevTools yourself.
Without React
@quantajs/core has no framework dependency. The same store works from plain
JavaScript:
const counter = useCounterStore();
const unsubscribe = counter.subscribe(() => {
document.querySelector('#count')!.textContent = String(counter.count);
});
counter.increment();