DevToolsManager
class DevToolsManager implements Manager
Integrates with Redux DevTools to track state and actions. Note: does not integrate time-travel.
Add the chrome extension or firefox extension to your browser to get started.
DevToolsManager implements Manager
constructor(options?, skipLogging?)
options
Arguments to send to redux devtools.
For example, we can enable the trace option to help track down where actions are dispatched from.
- Web
- React Native
- NextJS
- Expo
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
const managers = getDefaultManagers({
devToolsManager: { trace: true },
});
createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
const managers = getDefaultManagers({
devToolsManager: { trace: true },
});
const Root = () => (
<DataProvider managers={managers}>
<App />
</DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
'use client';
import { getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
const managers = getDefaultManagers({
devToolsManager: { trace: true },
});
export default function Provider({
children,
}: {
children: React.ReactNode;
}) {
return <DataProvider managers={managers}>{children}</DataProvider>;
}
import Provider from './Provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}
import { Stack } from 'expo-router';
import { DataProvider, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
devToolsManager: { trace: true },
});
export default function RootLayout() {
return (
<DataProvider managers={managers}>
<Stack>
<Stack.Screen name="index" />
</Stack>
</DataProvider>
);
}
skipLogging
(action: ActionTypes) => boolean
Can skip some actions to be registered in the browser devtool.
By default will skip inflight fetch actions
- Web
- React Native
- NextJS
- Expo
import {
DataProvider,
DevToolsManager,
getDefaultManagers,
} from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
// production builds leave out DevToolsManager
const managers = getDefaultManagers({
devToolsManager: new DevToolsManager(undefined, () => true),
});
createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);
import {
DataProvider,
DevToolsManager,
getDefaultManagers,
} from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
// production builds leave out DevToolsManager
const managers = getDefaultManagers({
devToolsManager: new DevToolsManager(undefined, () => true),
});
const Root = () => (
<DataProvider managers={managers}>
<App />
</DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
'use client';
import { DevToolsManager, getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
// production builds leave out DevToolsManager
const managers = getDefaultManagers({
devToolsManager: new DevToolsManager(undefined, () => true),
});
export default function Provider({
children,
}: {
children: React.ReactNode;
}) {
return <DataProvider managers={managers}>{children}</DataProvider>;
}
import Provider from './Provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}
import { Stack } from 'expo-router';
import {
DataProvider,
DevToolsManager,
getDefaultManagers,
} from '@data-client/react';
// production builds leave out DevToolsManager
const managers = getDefaultManagers({
devToolsManager: new DevToolsManager(undefined, () => true),
});
export default function RootLayout() {
return (
<DataProvider managers={managers}>
<Stack>
<Stack.Screen name="index" />
</Stack>
</DataProvider>
);
}
Skipping high-frequency updates
When using WebSockets or other real-time data sources,
high-frequency updates can overwhelm the DevTools extension. Use the predicate option to
filter out specific action types or schemas:
import { getDefaultManagers, actionTypes } from '@data-client/react';
import { Ticker } from './resources/Ticker';
const managers = getDefaultManagers({
devToolsManager: {
// Increase latency buffer for high-frequency updates
latency: 1000,
// Skip WebSocket SET actions for Ticker to reduce log spam
// (including batched set([Ticker], rows) writes)
predicate: (state, action) =>
action.type !== actionTypes.SET ||
(action.schema !== Ticker && action.schema[0] !== Ticker),
},
});
Programmatic store access
In development mode, DevToolsManager puts the page's store on globalThis.__DC__: __DC__.controller is its
Controller and __DC__.actions its most recent dispatched actions, including the ones from
page load. This works in browsers, React Native, and Node, with or without the Redux DevTools extension.
__DC__.controller.getState();
// Last 10 actions
__DC__.actions.slice(-10);
// Response for a key in the action log, without importing its endpoint
const c = __DC__.controller;
const a = __DC__.actions.findLast(
a => a.args && !a.endpoint?.sideEffect && a.key?.includes('/todos'),
);
c.getResponse(a.endpoint, ...a.args, c.getState()).data;
// With several stores, pick one by key
__DC__.stores; // ['Data Client: My App', 'Data Client: My App (2)']
__DC__.store = 'Data Client: My App (2)';
This is useful for AI coding assistants using a browser tool that runs page JavaScript (such as
Cursor's built-in browser, Chrome DevTools MCP or Playwright MCP),
or Expo MCP to programmatically inspect and interact
with the store. Each DataProvider registers independently, so
multiple providers on the same page are supported. When a name is already taken, the next one
registers as 'Data Client: My App (2)', and so on; set name to give each a recognizable key. Where there is no window, as when
server rendering, a store replaces the previous one with its name instead, since per-request stores are never cleaned up.
The action log keeps up to twice maxAge
actions (100 by default) and skips the same duplicate fetches the extension does. To keep its memory small, only the
newest 20 actions keep their response; older ones keep type, key, endpoint, meta, error
responses and the args of endpoints without side effects. While an endpoint polls, the log keeps only its last
maxPollingActions actions for each key (endpoint and args) (4 by default, the last two polls), so polling doesn't push everything else out:
- Web
- React Native
- NextJS
- Expo
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { createRoot } from 'react-dom/client';
import App from './App';
const managers = getDefaultManagers({
devToolsManager: { maxPollingActions: 10 },
});
createRoot(document.body).render(
<DataProvider managers={managers}>
<App />
</DataProvider>,
);
import { DataProvider, getDefaultManagers } from '@data-client/react';
import { AppRegistry } from 'react-native';
import App from './App';
const managers = getDefaultManagers({
devToolsManager: { maxPollingActions: 10 },
});
const Root = () => (
<DataProvider managers={managers}>
<App />
</DataProvider>
);
AppRegistry.registerComponent('MyApp', () => Root);
'use client';
import { getDefaultManagers } from '@data-client/react';
import { DataProvider } from '@data-client/react/nextjs';
const managers = getDefaultManagers({
devToolsManager: { maxPollingActions: 10 },
});
export default function Provider({
children,
}: {
children: React.ReactNode;
}) {
return <DataProvider managers={managers}>{children}</DataProvider>;
}
import Provider from './Provider';
export default function RootLayout({ children }) {
return (
<html>
<body>
<Provider>{children}</Provider>
</body>
</html>
);
}
import { Stack } from 'expo-router';
import { DataProvider, getDefaultManagers } from '@data-client/react';
const managers = getDefaultManagers({
devToolsManager: { maxPollingActions: 10 },
});
export default function RootLayout() {
return (
<DataProvider managers={managers}>
<Stack>
<Stack.Screen name="index" />
</Stack>
</DataProvider>
);
}
Controllers and their logs are removed when cleanup() is called. None of this exists in production builds.
More info
Using this Manager allows in browser debugging and store inspection.