|
| 1 | +# Debugging @data-client/react with Chrome DevTools MCP |
| 2 | + |
| 3 | +Use `user-chrome-devtools` MCP tools to programmatically inspect and interact with the |
| 4 | +data-client store in a running browser. Requires dev mode (`NODE_ENV !== 'production'`). |
| 5 | + |
| 6 | +For full [Controller](./Controller.md) API (fetch, set, invalidate, etc.) and |
| 7 | +[Action](./Actions.md) types (FETCH, SET, INVALIDATE, etc.), see those references. |
| 8 | + |
| 9 | +## Setup: Inject Action Logger |
| 10 | + |
| 11 | +On first navigation (or reload), inject a logging shim via `initScript` to capture all |
| 12 | +dispatched actions before the app boots. This gives you a persistent action log to query. |
| 13 | + |
| 14 | +```js |
| 15 | +navigate_page → initScript: |
| 16 | + |
| 17 | +window.__DC_ACTION_LOG__ = []; |
| 18 | +const _origDispatch = Object.getOwnPropertyDescriptor(Object.getPrototypeOf({}), 'dispatch'); |
| 19 | +Object.defineProperty(globalThis, '__DC_INTERCEPT_DISPATCH__', { |
| 20 | + value(action) { |
| 21 | + const entry = { |
| 22 | + type: action.type, |
| 23 | + key: action.meta?.key, |
| 24 | + schema: action.endpoint?.name ?? action.schema?.constructor?.name, |
| 25 | + timestamp: Date.now(), |
| 26 | + }; |
| 27 | + if (action.error) entry.error = true; |
| 28 | + globalThis.__DC_ACTION_LOG__.push(entry); |
| 29 | + if (globalThis.__DC_ACTION_LOG__.length > 500) |
| 30 | + globalThis.__DC_ACTION_LOG__ = globalThis.__DC_ACTION_LOG__.slice(-250); |
| 31 | + }, |
| 32 | + writable: true, configurable: true, |
| 33 | +}); |
| 34 | +``` |
| 35 | +
|
| 36 | +Then after the app loads, hook into the controller's dispatch. First discover |
| 37 | +the `devtoolsName` key (see "Accessing the Controller"), then use it: |
| 38 | +
|
| 39 | +```js |
| 40 | +evaluate_script: |
| 41 | + |
| 42 | +() => { |
| 43 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 44 | + if (!ctrl) return 'no controller yet'; |
| 45 | + const orig = ctrl._dispatch.bind(ctrl); |
| 46 | + ctrl._dispatch = (action) => { |
| 47 | + globalThis.__DC_INTERCEPT_DISPATCH__?.(action); |
| 48 | + return orig(action); |
| 49 | + }; |
| 50 | + return 'dispatch intercepted'; |
| 51 | +} |
| 52 | +``` |
| 53 | +
|
| 54 | +## Accessing the Controller |
| 55 | +
|
| 56 | +`DevToolsManager` registers controllers in `globalThis.__DC_CONTROLLERS__` (a `Map`) keyed by |
| 57 | +`devtoolsName` — defaults to `"Data Client: <page title>"`. |
| 58 | +
|
| 59 | +**Step 1: Discover available controllers and their keys.** |
| 60 | +
|
| 61 | +```js |
| 62 | +() => { |
| 63 | + const m = globalThis.__DC_CONTROLLERS__; |
| 64 | + if (!m || m.size === 0) return 'no controllers registered'; |
| 65 | + return [...m.keys()]; |
| 66 | +} |
| 67 | +``` |
| 68 | +
|
| 69 | +**Step 2: Get a controller by its `devtoolsName` key.** Use the key from Step 1. |
| 70 | +
|
| 71 | +```js |
| 72 | +() => { |
| 73 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 74 | + return ctrl ? 'controller found' : 'not found'; |
| 75 | +} |
| 76 | +``` |
| 77 | +
|
| 78 | +Always use `.get(devtoolsName)` with the actual key from Step 1 — not `.values().next().value` |
| 79 | +— so you target the correct store when multiple `DataProvider`s exist. |
| 80 | +
|
| 81 | +## Reading State |
| 82 | +
|
| 83 | +The store state shape is `{ entities, endpoints, indexes, meta, entitiesMeta, optimistic, lastReset }`. |
| 84 | +
|
| 85 | +### Full state overview |
| 86 | +
|
| 87 | +```js |
| 88 | +() => { |
| 89 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 90 | + if (!state) return 'no state'; |
| 91 | + return { |
| 92 | + entityTypes: Object.keys(state.entities), |
| 93 | + endpointCount: Object.keys(state.endpoints).length, |
| 94 | + optimisticCount: state.optimistic.length, |
| 95 | + lastReset: state.lastReset, |
| 96 | + }; |
| 97 | +} |
| 98 | +``` |
| 99 | +
|
| 100 | +### List all entities of a type |
| 101 | +
|
| 102 | +```js |
| 103 | +() => { |
| 104 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 105 | + const entities = state?.entities?.['Todo']; |
| 106 | + if (!entities) return 'no Todo entities'; |
| 107 | + return Object.values(entities); |
| 108 | +} |
| 109 | +``` |
| 110 | +
|
| 111 | +### Inspect a specific entity by pk |
| 112 | +
|
| 113 | +```js |
| 114 | +() => { |
| 115 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 116 | + return state?.entities?.['Todo']?.['5']; |
| 117 | +} |
| 118 | +``` |
| 119 | +
|
| 120 | +### List cached endpoint keys |
| 121 | +
|
| 122 | +```js |
| 123 | +() => { |
| 124 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 125 | + return Object.keys(state?.endpoints ?? {}); |
| 126 | +} |
| 127 | +``` |
| 128 | +
|
| 129 | +### Get endpoint response (raw, before denormalization) |
| 130 | +
|
| 131 | +```js |
| 132 | +() => { |
| 133 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 134 | + const key = Object.keys(state?.endpoints ?? {}).find(k => k.includes('GET /todos')); |
| 135 | + return key ? { key, data: state.endpoints[key] } : 'not found'; |
| 136 | +} |
| 137 | +``` |
| 138 | +
|
| 139 | +### Check endpoint metadata (expiry, errors, invalidation) |
| 140 | +
|
| 141 | +```js |
| 142 | +() => { |
| 143 | + const state = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App')?.getState(); |
| 144 | + const key = Object.keys(state?.meta ?? {}).find(k => k.includes('/todos')); |
| 145 | + return key ? { key, ...state.meta[key] } : 'no meta found'; |
| 146 | +} |
| 147 | +``` |
| 148 | +
|
| 149 | +## Dispatching Actions |
| 150 | +
|
| 151 | +Use the [Controller](./Controller.md) to mutate the store. Always look up by `devtoolsName`. |
| 152 | +See [Actions](./Actions.md) for the full list of action types dispatched. |
| 153 | +
|
| 154 | +### Reset the entire store |
| 155 | +
|
| 156 | +```js |
| 157 | +() => { |
| 158 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 159 | + ctrl?.resetEntireStore(); |
| 160 | + return 'store reset'; |
| 161 | +} |
| 162 | +``` |
| 163 | +
|
| 164 | +### Invalidate all endpoints (force refetch) |
| 165 | +
|
| 166 | +```js |
| 167 | +() => { |
| 168 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 169 | + ctrl?.invalidateAll({ testKey: () => true }); |
| 170 | + return 'all invalidated'; |
| 171 | +} |
| 172 | +``` |
| 173 | +
|
| 174 | +### Invalidate endpoints matching a pattern |
| 175 | +
|
| 176 | +```js |
| 177 | +() => { |
| 178 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 179 | + ctrl?.invalidateAll({ testKey: key => key.includes('/todos') }); |
| 180 | + return 'todo endpoints invalidated'; |
| 181 | +} |
| 182 | +``` |
| 183 | +
|
| 184 | +### Expire endpoints (mark stale, refetch on next use) |
| 185 | +
|
| 186 | +```js |
| 187 | +() => { |
| 188 | + const ctrl = globalThis.__DC_CONTROLLERS__?.get('Data Client: My App'); |
| 189 | + ctrl?.expireAll({ testKey: key => key.includes('/todos') }); |
| 190 | + return 'todo endpoints expired'; |
| 191 | +} |
| 192 | +``` |
| 193 | +
|
| 194 | +## Reading the Action Log |
| 195 | +
|
| 196 | +After injecting the dispatch interceptor (see Setup): |
| 197 | +
|
| 198 | +### Recent actions |
| 199 | +
|
| 200 | +```js |
| 201 | +() => globalThis.__DC_ACTION_LOG__?.slice(-20) ?? 'no log' |
| 202 | +``` |
| 203 | +
|
| 204 | +### Filter by type |
| 205 | +
|
| 206 | +```js |
| 207 | +() => globalThis.__DC_ACTION_LOG__?.filter(a => a.type === 'rdc/set') ?? [] |
| 208 | +``` |
| 209 | +
|
| 210 | +### Filter errors |
| 211 | +
|
| 212 | +```js |
| 213 | +() => globalThis.__DC_ACTION_LOG__?.filter(a => a.error) ?? [] |
| 214 | +``` |
| 215 | +
|
| 216 | +## Correlating with Network Requests |
| 217 | +
|
| 218 | +Use `list_network_requests` with `resourceTypes: ["fetch", "xhr"]` to see API calls, |
| 219 | +then cross-reference with endpoint keys in state. |
| 220 | +
|
| 221 | +## Debugging Checklist |
| 222 | +
|
| 223 | +1. **Verify controller exists**: Check `__DC_CONTROLLERS__` map size |
| 224 | +2. **Inspect state shape**: Get entity types and endpoint count |
| 225 | +3. **Check specific data**: Look up entities by type and pk |
| 226 | +4. **Review endpoint metadata**: Check expiry, errors, invalidation status |
| 227 | +5. **Track actions**: Read the action log for recent dispatches |
| 228 | +6. **Correlate network**: Compare `list_network_requests` with endpoint keys |
| 229 | +7. **Force refresh**: Use `invalidateAll` or `expireAll` to trigger refetches |
0 commit comments