Sunday, November 16, 2025
Common QuantaJS Mistakes and How to Avoid Them
Posted by
Common QuantaJS Mistakes and How to Avoid Them
QuantaJS 3.x keeps store definitions lightweight and resolves state through containers. Most mistakes come from blurring those two concepts, subscribing more broadly than a component needs, or treating server state like browser state.
This guide focuses on the current 3.x API.
1. Forgetting this in actions
❌ The mistake
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
count++;
},
},
});
✅ The solution
const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++;
},
},
});
Actions are bound to the resolved store, so this is how an action reads and writes its own state, getters, and other actions.
2. Resolving a whole store when one value is enough
❌ The mistake
function CounterLabel() {
const counter = useQuanta(useCounterStore);
return <span>{counter.count}</span>;
}
This is correct, but it subscribes the component to the whole store.
✅ The solution
function CounterLabel() {
const count = useQuantaValue(useCounterStore, (state) => state.count);
return <span>{count}</span>;
}
Use useQuantaValue when a component only needs a slice of state.
3. Using state hooks in a dispatch-only component
❌ The mistake
function IncrementButton() {
const counter = useQuanta(useCounterStore);
return <button onClick={() => counter.increment()}>Increment</button>;
}
The component does not render state, but it still subscribes to it.
✅ The solution
function IncrementButton() {
const counter = useQuantaActions(useCounterStore);
return <button onClick={() => counter.increment()}>Increment</button>;
}
useQuantaActions is the right fit for dispatch-only UI.
4. Treating QuantaProvider like a registry of stores
Store definitions do not need to be enumerated up front. They resolve lazily against the provider's container.
✅ Current pattern
function App({ children }: { children: React.ReactNode }) {
return <QuantaProvider>{children}</QuantaProvider>;
}
When server-rendered state needs to cross a boundary, pass a snapshot:
<QuantaProvider snapshot={snapshot}>{children}</QuantaProvider>
5. Mutating state from arbitrary component code
A resolved QuantaJS store is reactive, but scattering writes throughout the UI makes behavior harder to understand and test.
❌ The mistake
function TodoButton() {
const todos = useQuanta(useTodosStore);
return (
<button
onClick={() => {
todos.items.push({ id: crypto.randomUUID(), text: 'New todo' });
}}
>
Add
</button>
);
}
✅ The solution
const useTodosStore = defineStore('todos', {
state: () => ({
items: [] as Array<{ id: string; text: string }>,
}),
actions: {
addTodo(text: string) {
this.items.push({ id: crypto.randomUUID(), text });
},
},
});
function TodoButton() {
const todos = useQuantaActions(useTodosStore);
return <button onClick={() => todos.addTodo('New todo')}>Add</button>;
}
Actions give mutations a stable name and one testable home.
6. Recomputing reusable derived state in components
❌ The mistake
function TodoStats() {
const todos = useQuantaValue(useTodosStore, (state) => state.items);
const completed = todos.filter((todo) => todo.done).length;
return <span>{completed}</span>;
}
✅ The solution
const useTodosStore = defineStore('todos', {
state: () => ({
items: [] as Array<{ id: string; done: boolean }>,
}),
getters: {
completedCount: (state) =>
state.items.filter((todo) => todo.done).length,
},
});
function TodoStats() {
const completed = useQuantaValue(
useTodosStore,
(state) => state.completedCount,
);
return <span>{completed}</span>;
}
Put reusable derivations in getters so every component shares the same rule.
7. Watching more state than the side effect needs
❌ The mistake
watch(
() => settings,
() => syncTheme(),
);
✅ The solution
watch(
() => settings.theme,
(theme) => applyTheme(theme),
);
QuantaJS 3.x also supports a custom equality function for watch, which is useful when a source returns a small object or tuple.
8. Forgetting watcher cleanup
❌ The mistake
useEffect(() => {
watch(() => store.value, logValue);
}, [store]);
✅ The solution
useEffect(() => {
const stop = watch(() => store.value, logValue);
return stop;
}, [store]);
Long-lived watchers should be disposed with the component or lifecycle that created them.
9. Persisting more state than necessary
❌ The mistake
const useAppStore = defineStore('app', {
state: () => ({
user: null,
temporaryResults: [] as Result[],
cache: {} as Record<string, Result>,
}),
persist: {
adapter: new LocalStorageAdapter('app'),
},
});
✅ The solution
const useAppStore = defineStore('app', {
state: () => ({
user: null as User | null,
preferences: { theme: 'system' },
temporaryResults: [] as Result[],
}),
persist: {
adapter: new LocalStorageAdapter('app'),
include: ['user', 'preferences'],
debounceMs: 300,
},
});
Persist only state that must survive reloads. In 3.x the persistence configuration is typed from the store state.
10. Ignoring async action lifecycle state
You do not need a parallel loading flag for every async action.
const useUserStore = defineStore('user', {
state: () => ({
profile: null as User | null,
}),
actions: {
async load(id: string) {
const response = await fetch(`/api/users/${id}`, {
signal: this.$signal,
});
this.profile = await response.json();
},
},
});
function UserPanel() {
const user = useQuanta(useUserStore);
if (user.load.pending) return <p>Loading…</p>;
if (user.load.error) return <p>Could not load the user.</p>;
return <p>{user.profile?.name}</p>;
}
Use the action's built-in pending, error, and abort support instead of duplicating that state by hand.
11. Creating a component-owned store incorrectly
A store definition should stay at module scope. Component-local ownership comes from useLocalStore.
✅ The solution
const useDraftStore = defineStore('draft', {
state: () => ({ text: '' }),
actions: {
setText(text: string) {
this.text = text;
},
},
});
function DraftEditor() {
const draft = useLocalStore(useDraftStore);
return (
<textarea
value={draft.text}
onChange={(event) => draft.setText(event.target.value)}
/>
);
}
Two mounted editors get separate state without creating definitions during render.
12. Returning unstable selector objects without equality
❌ The mistake
const summary = useQuantaValue(
useUserStore,
(state) => ({
name: state.name,
email: state.email,
}),
);
The selector creates a new object each time it runs.
✅ The solution
import { shallow } from '@quantajs/core';
const summary = useQuantaValue(
useUserStore,
(state) => ({
name: state.name,
email: state.email,
}),
{ equalityFn: shallow },
);
Or select primitive values separately when that is simpler.
13. Putting unrelated domains in one store
❌ The mistake
const useEverythingStore = defineStore('app', {
state: () => ({
user: null,
cart: [],
todos: [],
theme: 'system',
searchResults: [],
}),
});
✅ The solution
Create domain stores such as useUserStore, useCartStore, and useSettingsStore.
Smaller stores make ownership clearer and help components subscribe only to what they use.
14. Assuming every store should use the ambient container on a server
This is the most important server-side mistake in 3.x.
A Node server handles many users in one process. Resolving a store against shared ambient state can leak one request's data into another.
❌ The mistake
export async function GET() {
const user = useUserStore();
user.profile = await loadCurrentUser();
return Response.json(user.profile);
}
✅ The solution
import { createContainer } from '@quantajs/core';
export async function GET() {
const container = createContainer();
try {
const user = useUserStore(container);
user.profile = await loadCurrentUser();
return Response.json(container.dehydrate());
} finally {
container.dispose();
}
}
Create one container per request, resolve server-side stores against it explicitly, then dispose it.
For frameworks that need an ambient server resolver, use a request-scoped resolver such as setDefaultContainerResolver backed by request-local context rather than a process-wide singleton.
15. Sharing one container between tests
Tests should not inherit state from the previous test.
✅ The solution
import { createContainer } from '@quantajs/core';
describe('counter', () => {
let container: ReturnType<typeof createContainer>;
beforeEach(() => {
container = createContainer();
});
afterEach(() => {
container.dispose();
});
it('increments', () => {
const counter = useCounterStore(container);
counter.increment();
expect(counter.count).toBe(1);
});
});
A fresh container per test is deterministic and avoids cleanup-by-reset conventions.
16. Forgetting that snapshots must be serializable
A server snapshot commonly crosses a framework serialization boundary.
❌ Risky state
state: () => ({
lastSeen: new Date(),
selected: new Set<string>(),
})
✅ Safer state
state: () => ({
lastSeenIso: new Date().toISOString(),
selectedIds: [] as string[],
})
Normalize non-serializable values before dehydration when the snapshot will be sent to a client.
17. Optimizing before measuring
A broad subscription is not automatically a bug, and splitting every value into its own selector can make code harder to read.
Start with the clearest correct code, use browser performance tools to find hot components, then narrow the subscriptions that actually matter.
Quick reference
Prefer
const count = useQuantaValue(useCounterStore, (state) => state.count);
const actions = useQuantaActions(useCounterStore);
const draft = useLocalStore(useDraftStore);
On the server
const container = createContainer();
const store = useUserStore(container);
In tests
const container = createContainer();
const store = useCounterStore(container);
container.dispose();
Conclusion
The safest QuantaJS 3.x code follows a few consistent rules:
- Define stores once with
defineStore. - Resolve state through the right container.
- Subscribe as narrowly as the component needs.
- Put mutations in actions and reusable derivations in getters.
- Use
useLocalStorefor component-owned state. - Create a fresh container per request and per test.
- Keep persisted and dehydrated state intentionally small.
Those boundaries make state easier to reason about, easier to test, and safer across both browser and server runtimes.