DevTools

QuantaJS DevTools provides a powerful visual debugging interface for inspecting stores, monitoring state changes, and tracking actions in real-time. Whether you're building a small prototype or a large-scale application, DevTools helps you understand and debug your state management with ease.

DevTools is opt-in as of 2.1:

Nothing is collected and nothing is attached to window until you call enableDevTools(). This is a security change, not an ergonomic one — see Why it is opt-in.

Features

Real-time Store Inspector – View live state, getters, and actions
Action Log – Track all state mutations with timestamps and payloads
Opt-In – Collects nothing until explicitly enabled
Redaction – Mask sensitive state paths and action arguments
Framework Agnostic – Works with vanilla JS, React, or any framework
Modern UI – Built with Preact and Shadow DOM, so it cannot leak styles into your app
Out of Your Bundle – An optional peer dependency behind a dynamic import
Persistence Management – View and manage persistence status for stores

Two pieces

DevTools is split in two, and you need both:

PieceFromDoes
The bridgeenableDevTools() in @quantajs/coreCollects store state and action calls. Off by default.
The UImountDevTools() in @quantajs/devtools, or <QuantaDevTools /> in @quantajs/react/devtoolsRenders the panel.

Mount the UI without enabling the bridge and the panel appears empty — that is the symptom to recognise.

Installation

Install the DevTools package alongside the core library:

npm install @quantajs/devtools @quantajs/core
# or
yarn add @quantajs/devtools @quantajs/core
# or
pnpm add @quantajs/devtools @quantajs/core

Quick Start

import { defineStore, enableDevTools } from '@quantajs/core';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  actions: {
    increment() {
      this.count++;
    },
  },
});

// 1. Turn on the bridge — only in development.
if (process.env.NODE_ENV === 'development') {
  enableDevTools();
}
// 2. Mount the UI.
import { mountDevTools } from '@quantajs/devtools';

mountDevTools();

A floating button appears in the bottom-right corner. Click it to open the panel.

Enable the bridge as early as possible — ideally at your app's entry point, before any store is created. It picks up stores created afterwards; stores created before it is enabled may not be registered.

Redacting sensitive values

DevTools observes everything: full store contents and every argument passed to every action. When that includes tokens or personal data, redact it:

enableDevTools({
  redact: ['token', 'user.ssn', 'payment.cardNumber'],
});

Paths are matched against state values and against action arguments that are objects carrying a matching key. Matches are replaced with [redacted].

Turning it back off

import { disableDevTools } from '@quantajs/core';

disableDevTools();

Mounting options

import { mountDevTools } from '@quantajs/devtools';

// Force the panel visible regardless of environment detection
mountDevTools({ visible: true });

// Mount into a specific element
mountDevTools({
  target: document.getElementById('devtools-container'),
});

Note:

mountDevTools({ visible }) controls only whether the panel renders. It has no effect on data collection — that is enableDevTools(), and its default is off. The two functions take different option objects that happen to share a type name.

Using DevTools

Opening DevTools

Once mounted, you'll see a floating button with the QuantaJS logo in the bottom-right corner of your application. Click it to open the DevTools panel.

Store Inspector

The Inspector tab provides a comprehensive view of your stores:

DevTools Store Inspector

State Viewing

  • Live State: See your store state update in real-time as you interact with your application
  • Nested Objects: Expandable tree view for complex state structures
  • Color-Coded Values: Different colors for keys, values, and types make it easy to scan

Getters

View all computed values (getters) alongside their current values. Getters are displayed with their computed results, making it easy to verify that your derived state is working correctly.

Actions

See all available actions for the selected store. You can even trigger actions directly from the DevTools by clicking the play button next to each action.

Store Management

  • Persistence Status: See at a glance whether persistence is enabled for a store
  • Clear Storage: Remove persisted data from storage (useful for testing)
  • Reset State: Reset the store to its initial state values

Action Log

The Actions tab provides a complete history of all actions that have been dispatched:

DevTools Action Log

  • Timestamps: See exactly when each action was called
  • Store Context: Know which store each action belongs to
  • Payloads: View the arguments passed to each action
  • Expandable Payloads: Large payloads can be expanded to see full details
  • Copy Payload: Copy action payloads to clipboard for debugging

The action log keeps track of the last 100 actions, giving you a comprehensive view of your application's state changes.

React Integration

If you're using React, you can use the QuantaDevTools component from @quantajs/react/devtools:

import { enableDevTools } from '@quantajs/react';
import { QuantaDevTools } from '@quantajs/react/devtools';

if (process.env.NODE_ENV === 'development') {
  enableDevTools();
}

function App() {
  return (
    <div>
      <YourApp />
      <QuantaDevTools />
    </div>
  );
}

The component handles mounting and unmounting for you. @quantajs/devtools is an optional peer dependency loaded through a dynamic import, so it stays out of your production bundle — which is why @quantajs/react is 2.8 kB gzipped rather than the 20.1 kB it was in 2.0.0.

It moved in 2.1.1:

In 2.1.0 this component was exported from @quantajs/react itself. Because it dynamically imports @quantajs/devtools — a specifier bundlers resolve at build time — that broke the build of every application which had not installed that optional peer, whether or not it used DevTools at all. Importing from the /devtools subpath is what confines the requirement to the apps that want the panel. The old import still compiles but renders nothing and logs a warning.

In the Next.js App Router, put this in a file marked 'use client'.

Development vs Production

Why it is opt-in

Before 2.1, the DevTools bridge enabled itself whenever process.env.NODE_ENV was not statically replaced at build time, and attached itself to window at import time. That check holds under Vite, webpack and Next.js — and fails everywhere else: a CDN bundle, Deno, a plain <script type="module">. In those environments the bridge stayed on in production, exposing every store's full contents and every action argument (credentials, tokens, personal data) to any script running on the page.

Environment detection was the wrong mechanism for a security boundary. As of 2.1 the bridge attaches only after an explicit enableDevTools() call, so there is no environment in which it turns itself on.

Keeping it out of production

if (process.env.NODE_ENV === 'development') {
  enableDevTools();
}
// Vite
if (import.meta.env.DEV) {
  enableDevTools();
}

The UI's own visibility check still auto-detects, so mountDevTools() with no options will not render the panel in a production build. Treat that as a convenience, not a guarantee — the guard that matters is the one around enableDevTools().

Tree-Shaking

For production builds, you can exclude DevTools entirely using conditional imports:

if (process.env.NODE_ENV === 'development') {
  import('@quantajs/devtools').then(({ mountDevTools }) => {
    mountDevTools();
  });
}

Or with Vite:

if (import.meta.env.DEV) {
  import('@quantajs/devtools').then(({ mountDevTools }) => {
    mountDevTools();
  });
}

Advanced Usage

Custom Mount Target

Mount DevTools to a specific element instead of the body:

const container = document.getElementById('devtools-container');
mountDevTools({ target: container });

Cleanup

The mountDevTools function returns a cleanup function that you can call to unmount DevTools:

const cleanup = mountDevTools({ visible: true });

// Later, when you want to remove DevTools
cleanup();

Multiple Instances

While not recommended, you can mount multiple DevTools instances. Each will connect to the same store registry, so they'll show the same data.

Store Inspector Features

Real-Time Updates

The Store Inspector automatically updates when store state changes. You don't need to refresh or manually update the view—changes appear instantly.

Search and Filter

Use the search box in the sidebar to quickly find stores by name. This is especially useful when you have many stores in your application.

Store Selection

Click on any store in the sidebar to inspect it. The main panel will update to show that store's state, getters, and actions.

State Tree Navigation

Complex nested state is displayed as an expandable tree. Click through nested objects to explore your state structure.

Action Log Features

Action History

The Action Log maintains a history of the last 100 actions. Each action includes:

  • Time: When the action was called
  • Store: Which store the action belongs to
  • Action Name: The name of the action that was called
  • Payload: The arguments passed to the action

Payload Inspection

  • Truncated View: Long payloads are truncated with an ellipsis
  • Expand: Click the expand button to see the full payload
  • Copy: Copy the payload JSON to your clipboard for debugging

Action Filtering

While the Action Log doesn't currently support filtering, you can use the browser's find feature (Ctrl+F / Cmd+F) to search through actions.

Best Practices

Development Only

Guard enableDevTools(), and load the UI through a dynamic import so it never enters your production bundle:

if (process.env.NODE_ENV === 'development') {
  enableDevTools();
  const { mountDevTools } = await import('@quantajs/devtools');
  mountDevTools();
}

Redact Before You Need To

Add redact entries when you add the field, not after a screenshot has already gone into a bug report:

enableDevTools({ redact: ['token', 'refreshToken', 'user.email'] });

Store Naming

Use descriptive store names to make it easier to identify stores in DevTools:

// ✅ Good - descriptive names
const useUserStore = defineStore('user', { /* ... */ });
const useCartStore = defineStore('shopping-cart', { /* ... */ });

// ❌ Bad - unclear names
const useStore1 = defineStore('s1', { /* ... */ });
const useDataStore = defineStore('data', { /* ... */ });

Action Naming

Use clear action names that describe what they do:

// ✅ Good - clear action names
actions: {
  incrementCounter() { /* ... */ },
  addItemToCart(item) { /* ... */ },
  updateUserProfile(profile) { /* ... */ },
}

// ❌ Bad - unclear action names
actions: {
  do() { /* ... */ },
  update() { /* ... */ },
  change(data) { /* ... */ },
}

Debugging Workflow

  1. Open DevTools when you start development
  2. Monitor Actions to understand the flow of state changes
  3. Inspect State to verify that state updates are correct
  4. Check Getters to ensure computed values are working
  5. Use Action Log to trace back through state changes when debugging issues

Troubleshooting

The panel is empty

This is the most common problem after upgrading to 2.1, and it has one cause: enableDevTools() was never called. The UI mounts fine, but the bridge collects nothing, so there is nothing to show.

DevTools not appearing at all

  1. Check the environment: mountDevTools() with no options only renders when it detects development. Pass { visible: true } to force it.
  2. Check the mount: verify mountDevTools() is actually running.
  3. Check the console for errors.
  4. Check the target: if you passed a target, make sure the element exists at mount time.

Some stores are missing

  1. enableDevTools() ran too late. It picks up stores created after it is enabled. Move it to your entry point, before any store is created.
  2. The store is in a different container. A store in a per-request or useLocalStore container has its own lifetime and may already be disposed.
  3. Duplicate names. Two stores sharing a name in one container are one store — the second resolution returns the first.

Actions not logging

  1. The bridge is off — see above.
  2. Verify the action is being called, not shadowed by a state key of the same name.
  3. Check redact — a redacted payload still logs, but shows [redacted].

Learn More