DevTools Tutorial
devtools() connects a store proxy to the Redux DevTools browser extension for state inspection and time-travel debugging. Every mutation sends a snapshot to the DevTools panel. You can inspect state, jump to any point in history, and the store updates in real-time.
Prerequisites
Install the Redux DevTools extension for your browser:
Getting Started
1. Create a store
import {createClassyStore} from '@codebelt/classy-store';
class CounterStore {
count = 0;
step = 1;
get doubled() {
return this.count * 2;
}
increment() {
this.count += this.step;
}
decrement() {
this.count -= this.step;
}
setStep(step: number) {
this.step = step;
}
}
export const counterStore = createClassyStore(new CounterStore());
2. Connect to DevTools
import {devtools} from '@codebelt/classy-store/utils';
const disconnect = devtools(counterStore, {name: 'CounterStore'});
Open the Redux DevTools panel in your browser. You'll see CounterStore in the instance selector with the initial state: { count: 0, step: 1 }.
3. Mutate and observe
counterStore.increment();
// DevTools shows: action "STORE_UPDATE", state { count: 1, step: 1 }
counterStore.increment();
// DevTools shows: action "STORE_UPDATE", state { count: 2, step: 1 }
counterStore.setStep(5);
counterStore.increment();
// DevTools shows: action "STORE_UPDATE", state { count: 7, step: 5 }
Each batched mutation appears as a new entry in the DevTools timeline.
4. Time-travel
Click any previous entry in the DevTools timeline. The store proxy updates to match that state — your React components re-render, side effects fire, everything stays in sync. Click "Jump to State" or "Jump to Action" to navigate through history.
5. Disconnect when done
disconnect();
After disconnecting, mutations are no longer sent to DevTools and time-travel messages are ignored.
Signature
function devtools<T extends object>(
proxyStore: T,
options?: DevtoolsOptions,
): () => void;
Options
| Option | Type | Default | Description |
|---|---|---|---|
name | string | 'ClassyStore' | Display name in the DevTools panel |
enabled | boolean | true | Set to false to disable (returns a noop) |
Return value
A dispose function () => void that disconnects from DevTools and unsubscribes from the store.
How It Works
- Connect: calls
window.__REDUX_DEVTOOLS_EXTENSION__.connect({ name })to create a DevTools connection. - Init: sends the initial snapshot via
connection.init(snapshot(store)). - Subscribe: uses
subscribe(store, ...)to listen for mutations. On each batched mutation, sendssnapshot(store)to DevTools viaconnection.send(). - Time-travel: listens for
DISPATCHmessages from DevTools. When aJUMP_TO_STATEorJUMP_TO_ACTIONmessage arrives, parses the state and applies it back to the store proxy — skipping getters and methods (same pattern aspersist()). - Dispose: unsubscribes from the store and disconnects from DevTools.
Use Cases
Multiple stores
Connect each store with a unique name to distinguish them in the DevTools panel:
import {createClassyStore} from '@codebelt/classy-store';
import {devtools} from '@codebelt/classy-store/utils';
const authStore = createClassyStore(new AuthStore());
const todoStore = createClassyStore(new TodoStore());
const uiStore = createClassyStore(new UiStore());
devtools(authStore, {name: 'AuthStore'});
devtools(todoStore, {name: 'TodoStore'});
devtools(uiStore, {name: 'UiStore'});
Each store appears as a separate instance in the DevTools dropdown.
Development-only
Disable DevTools in production by tying the enabled option to your build environment:
devtools(counterStore, {
name: 'CounterStore',
enabled: import.meta.env.DEV,
});
When enabled is false, devtools() returns a noop immediately — no DevTools connection is created, no subscriptions are added, and the function has zero runtime cost.
Conditional connection
Connect DevTools only when the extension is available:
const disconnect = devtools(counterStore, {name: 'CounterStore'});
// If the extension isn't installed, `disconnect` is a noop.
// No errors, no warnings — the store works normally.
devtools() checks for window.__REDUX_DEVTOOLS_EXTENSION__ internally. If it's not available (missing extension, SSR, Node.js), it returns a noop without throwing.
Late disposal on unmount (React)
import {useEffect} from 'react';
import {devtools} from '@codebelt/classy-store/utils';
function App() {
useEffect(() => {
const disconnect = devtools(counterStore, {name: 'CounterStore'});
return () => disconnect();
}, []);
return <Counter />;
}
Time-Travel Details
When you click a previous state in the DevTools timeline, the extension sends a message like:
{
"type": "DISPATCH",
"payload": {"type": "JUMP_TO_STATE"},
"state": "{\"count\": 0, \"step\": 1}"
}
The devtools() utility:
- Parses
message.stateviaJSON.parse(). - Iterates over the keys of the parsed state.
- Skips getters — detected via
findGetterDescriptor()on the prototype chain. Getters likedoubledare computed values that recompute from the restored data automatically. - Skips methods — detected via
typeof value === 'function'. - Assigns data properties directly to the store proxy:
proxy[key] = value. - The SET trap fires, triggers reactivity, and your components update.
During time-travel, the utility temporarily pauses sending updates to DevTools to avoid echoing the applied state back as a new action.
Quick Reference
| What you want | How to do it |
|---|---|
| Connect a store | devtools(store, {name: 'MyStore'}) |
| Disconnect | const dispose = devtools(...); dispose() |
| Disable in production | enabled: import.meta.env.DEV |
| Multiple stores | Call devtools() once per store with unique names |
| Use without extension | Works safely — returns a noop if extension is missing |