diff --git a/README.md b/README.md index 99b3131119..9d4b180c17 100644 --- a/README.md +++ b/README.md @@ -428,6 +428,12 @@ If an action type is not provided, it is defaulted to "anonymous". You can custo devtools(..., { anonymousActionType: 'unknown', ... }) ``` +If you like to try to infer the action type from the function name, you can set `inferName` to `true`: + +```jsx +devtools(..., { inferActionName: true, ... }) +``` + If you wish to disable devtools (on production for instance). You can customize this setting by providing the `enabled` parameter: ```jsx diff --git a/src/middleware/devtools.ts b/src/middleware/devtools.ts index c5cd1195c8..546d760dad 100644 --- a/src/middleware/devtools.ts +++ b/src/middleware/devtools.ts @@ -1,4 +1,5 @@ import type {} from '@redux-devtools/extension' + import type { StateCreator, StoreApi, @@ -72,6 +73,7 @@ export interface DevtoolsOptions extends Config { name?: string enabled?: boolean anonymousActionType?: string + inferActionName?: boolean store?: string } @@ -109,6 +111,13 @@ type ConnectionInformation = { connection: Connection stores: Record } +const findCallerName = (stack: string) => { + const traceLines = stack?.split?.('\n') ?? [] + const isBlinkStackTrace = stack.startsWith('Error') + return isBlinkStackTrace + ? traceLines?.[2]?.trim().split(' ')[1] + : traceLines?.[1]?.trim().split('@')[0] +} const trackedConnections: Map = new Map() const getTrackedConnectionState = ( @@ -149,7 +158,8 @@ const extractConnectionInformation = ( const devtoolsImpl: DevtoolsImpl = (fn, devtoolsOptions = {}) => (set, get, api) => { - const { enabled, anonymousActionType, store, ...options } = devtoolsOptions + const { enabled, anonymousActionType, inferActionName, store, ...options } = + devtoolsOptions type S = ReturnType & { [store: string]: ReturnType @@ -178,9 +188,13 @@ const devtoolsImpl: DevtoolsImpl = ;(api.setState as any) = ((state, replace, nameOrAction: Action) => { const r = set(state, replace as any) if (!isRecording) return r + let defaultActionName = anonymousActionType + if (inferActionName) { + defaultActionName = findCallerName(new Error().stack ?? '') + } const action: { type: string } = nameOrAction === undefined - ? { type: anonymousActionType || 'anonymous' } + ? { type: defaultActionName || 'anonymous' } : typeof nameOrAction === 'string' ? { type: nameOrAction } : nameOrAction diff --git a/tests/devtools.test.tsx b/tests/devtools.test.tsx index 5ce6f877cc..0d9271a023 100644 --- a/tests/devtools.test.tsx +++ b/tests/devtools.test.tsx @@ -189,6 +189,36 @@ 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, + inferActionName: true, + } + + const api = createStore( + devtools( + (set) => ({ + count: 0, + setCount: (newCount: number) => { + set({ count: newCount }) + }, + }), + options, + ), + ) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + 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 () => {