Back to QuantaJS Blog

Sunday, November 16, 2025

Common QuantaJS Mistakes and How to Avoid Them

Cover image for Common QuantaJS Mistakes and How to Avoid Them

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 useLocalStore for 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.