Skip to content

add isAction helper function, and ensure listener middleware only runs for actions #3372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions packages/toolkit/src/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -241,7 +241,7 @@ export function createAction<P = void, T extends string = string>(
* 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.
Expand Down Expand Up @@ -286,15 +286,25 @@ 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<unknown> {
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
error?: unknown
meta?: unknown
} {
return (
isPlainObject(action) &&
typeof (action as any).type === 'string' &&
isAction(action) &&
typeof action.type === 'string' &&
Object.keys(action).every(isValidKey)
)
}
Expand Down
2 changes: 2 additions & 0 deletions packages/toolkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export {
// js
createAction,
getType,
isAction,
isFSA as isFluxStandardAction,
} from './createAction'
export type {
// types
Expand Down
7 changes: 6 additions & 1 deletion packages/toolkit/src/listenerMiddleware/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -426,6 +426,11 @@ export function createListenerMiddleware<

const middleware: ListenerMiddleware<S, D, ExtraArgument> =
(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)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/toolkit/src/listenerMiddleware/tests/fork.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ describe('fork', () => {
},
})

store.dispatch(increment)
store.dispatch(increment())

expect(await deferredResult).toBe(listenerCompleted)
})
Expand Down
23 changes: 22 additions & 1 deletion packages/toolkit/src/tests/createAction.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createAction, getType } from '@reduxjs/toolkit'
import { createAction, getType, isAction } from '@reduxjs/toolkit'

describe('createAction', () => {
it('should create an action', () => {
Expand Down Expand Up @@ -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')
Expand Down