diff --git a/packages/toolkit/src/createAction.ts b/packages/toolkit/src/createAction.ts index 0647a04621..ab52d1c4fc 100644 --- a/packages/toolkit/src/createAction.ts +++ b/packages/toolkit/src/createAction.ts @@ -224,7 +224,7 @@ export type PayloadActionCreator< * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function - * will also have its toString() overriden so that it returns the action type, + * will also have its toString() overridden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * * @param type The action type to use for created actions. @@ -241,7 +241,7 @@ export function createAction

( * A utility function to create an action creator for the given action type * string. The action creator accepts a single argument, which will be included * in the action object as a field called payload. The action creator function - * will also have its toString() overriden so that it returns the action type, + * will also have its toString() overridden so that it returns the action type, * allowing it to be used in reducer logic that is looking for that action type. * * @param type The action type to use for created actions. @@ -286,6 +286,16 @@ export function createAction(type: string, prepareAction?: Function): any { return actionCreator } +/** + * Returns true if value is a plain object with a `type` property. + */ +export function isAction(action: unknown): action is Action { + return isPlainObject(action) && 'type' in action +} + +/** + * Returns true if value is an action with a string type and valid Flux Standard Action keys. + */ export function isFSA(action: unknown): action is { type: string payload?: unknown @@ -293,8 +303,8 @@ export function isFSA(action: unknown): action is { meta?: unknown } { return ( - isPlainObject(action) && - typeof (action as any).type === 'string' && + isAction(action) && + typeof action.type === 'string' && Object.keys(action).every(isValidKey) ) } diff --git a/packages/toolkit/src/index.ts b/packages/toolkit/src/index.ts index 2ea3e1384c..1f32530046 100644 --- a/packages/toolkit/src/index.ts +++ b/packages/toolkit/src/index.ts @@ -39,6 +39,8 @@ export { // js createAction, getType, + isAction, + isFSA as isFluxStandardAction, } from './createAction' export type { // types diff --git a/packages/toolkit/src/listenerMiddleware/index.ts b/packages/toolkit/src/listenerMiddleware/index.ts index d96b9257db..56205295ee 100644 --- a/packages/toolkit/src/listenerMiddleware/index.ts +++ b/packages/toolkit/src/listenerMiddleware/index.ts @@ -1,6 +1,6 @@ import type { Dispatch, AnyAction, MiddlewareAPI } from 'redux' import type { ThunkDispatch } from 'redux-thunk' -import { createAction } from '../createAction' +import { createAction, isAction } from '../createAction' import { nanoid } from '../nanoid' import type { @@ -426,6 +426,11 @@ export function createListenerMiddleware< const middleware: ListenerMiddleware = (api) => (next) => (action) => { + if (!isAction(action)) { + // we only want to notify listeners for action objects + return next(action) + } + if (addListener.match(action)) { return startListening(action.payload) } diff --git a/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts b/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts index 2756e09f15..6d9e6abb56 100644 --- a/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts +++ b/packages/toolkit/src/listenerMiddleware/tests/fork.test.ts @@ -367,7 +367,7 @@ describe('fork', () => { }, }) - store.dispatch(increment) + store.dispatch(increment()) expect(await deferredResult).toBe(listenerCompleted) }) diff --git a/packages/toolkit/src/tests/createAction.test.ts b/packages/toolkit/src/tests/createAction.test.ts index b5e8b67b26..5b481b945c 100644 --- a/packages/toolkit/src/tests/createAction.test.ts +++ b/packages/toolkit/src/tests/createAction.test.ts @@ -1,4 +1,4 @@ -import { createAction, getType } from '@reduxjs/toolkit' +import { createAction, getType, isAction } from '@reduxjs/toolkit' describe('createAction', () => { it('should create an action', () => { @@ -122,6 +122,27 @@ describe('createAction', () => { }) }) +describe('isAction', () => { + it('should only return true for plain objects with a type property', () => { + const actionCreator = createAction('anAction') + class Action { + type = 'totally an action' + } + const testCases: [action: unknown, expected: boolean][] = [ + [{ type: 'an action' }, true], + [{ type: 'more props', extra: true }, true], + [actionCreator(), true], + [actionCreator, false], + [Promise.resolve({ type: 'an action' }), false], + [new Action(), false], + ['a string', false], + ] + for (const [action, expected] of testCases) { + expect(isAction(action)).toBe(expected) + } + }) +}) + describe('getType', () => { it('should return the action type', () => { const actionCreator = createAction('A_TYPE')