feat(devtool): inferred action type (#2987)

* feat(devtools middleware) add automatic action name finding

* docs(readme) add inferActionName example

* feat: update readmes

* feat: update devtools middleware and tests

* feat: remove inferActionName

---------

Co-authored-by: Daishi Kato <dai-shi@users.noreply.github.com>
Co-authored-by: Danilo Britto <dbritto.dev@gmail.com>
This commit is contained in:
Ali Mert Çakar 2025-05-21 14:12:14 +03:00 committed by GitHub
parent 6953c29dc5
commit 670b60e19a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 3 deletions

View File

@ -58,8 +58,8 @@ devtools<T>(stateCreatorFn: StateCreator<T, [], []>, devtoolsOptions?: DevtoolsO
- **optional** `enabled`: Defaults to `true` when is on development mode, and defaults to `false`
when is on production mode. Enables or disables the Redux DevTools integration
for this store.
- **optional** `anonymousActionType`: Defaults to `anonymous`. A string to use as the action type
for anonymous mutations in the Redux DevTools.
- **optional** `anonymousActionType`: Defaults to the inferred action type or `anonymous` if
unavailable. A string to use as the action type for anonymous mutations in the Redux DevTools.
- **optional** `store`: A custom identifier for the store in the Redux DevTools.
#### Returns

View File

@ -1,4 +1,5 @@
import type {} from '@redux-devtools/extension'
import type {
StateCreator,
StoreApi,
@ -112,6 +113,7 @@ type ConnectionInformation = {
connection: Connection
stores: Record<StoreName, StoreInformation>
}
const trackedConnections: Map<ConnectionName, ConnectionInformation> = new Map()
const getTrackedConnectionState = (
@ -162,6 +164,17 @@ const removeStoreFromTrackedConnections = (
}
}
const findCallerName = (stack: string | undefined) => {
if (!stack) return undefined
const traceLines = stack.split('\n')
const apiSetStateLineIndex = traceLines.findIndex((traceLine) =>
traceLine.includes('api.setState'),
)
if (apiSetStateLineIndex < 0) return undefined
const callerLine = traceLines[apiSetStateLineIndex + 1]?.trim() || ''
return /.+ (.+) .+/.exec(callerLine)?.[1]
}
const devtoolsImpl: DevtoolsImpl =
(fn, devtoolsOptions = {}) =>
(set, get, api) => {
@ -194,9 +207,10 @@ const devtoolsImpl: DevtoolsImpl =
;(api.setState as any) = ((state, replace, nameOrAction: Action) => {
const r = set(state, replace as any)
if (!isRecording) return r
const inferredActionType = findCallerName(new Error().stack)
const action: { type: string } =
nameOrAction === undefined
? { type: anonymousActionType || 'anonymous' }
? { type: anonymousActionType || inferredActionType || 'anonymous' }
: typeof nameOrAction === 'string'
? { type: nameOrAction }
: nameOrAction

View File

@ -193,6 +193,37 @@ describe('When state changes...', () => {
})
})
describe('When state changes with automatic setter inferring...', () => {
it("sends { type: setStateName || 'setCount`, ...rest } as the action with current state", async () => {
const options = {
name: 'testOptionsName',
enabled: true,
}
const api = createStore<{
count: number
setCount: (count: number) => void
}>()(
devtools(
(set) => ({
count: 0,
setCount: (newCount: number) => {
set({ count: newCount })
},
}),
options,
),
)
api.getState().setCount(10)
const [connection] = getNamedConnectionApis(options.name)
expect(connection.send).toHaveBeenLastCalledWith(
{ type: 'Object.setCount' },
{ count: 10, setCount: expect.any(Function) },
)
})
})
describe('when it receives a message of type...', () => {
describe('ACTION...', () => {
it('does nothing', async () => {