Skip to content

Commit 67eef52

Browse files
committed
create action creator middleware
1 parent 1754832 commit 67eef52

File tree

6 files changed

+167
-0
lines changed

6 files changed

+167
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { Middleware } from 'redux'
2+
import { isActionCreator as isRTKAction } from './createAction'
3+
4+
export interface ActionCreatorInvariantMiddlewareOptions {
5+
/**
6+
* The function to identify whether a value is an action creator.
7+
* The default checks for a function with a static type property and match method.
8+
*/
9+
isActionCreator?: (action: unknown) => action is Function & { type?: unknown }
10+
}
11+
12+
export function getMessage(type?: unknown) {
13+
return `Detected an action creator with type "${
14+
type || 'unknown'
15+
}" being dispatched.
16+
Make sure you're calling the action before dispatching, i.e. \`dispatch(actionCreator())\` instead of \`dispatch(actionCreator)\`. This is necessary even if the action has no payload.`
17+
}
18+
19+
export function createActionCreatorInvariantMiddleware(
20+
options: ActionCreatorInvariantMiddlewareOptions = {}
21+
): Middleware {
22+
if (process.env.NODE_ENV === 'production') {
23+
return () => (next) => (action) => next(action)
24+
}
25+
const { isActionCreator = isRTKAction } = options
26+
return () => (next) => (action) => {
27+
if (isActionCreator(action)) {
28+
console.warn(getMessage(action.type))
29+
}
30+
return next(action)
31+
}
32+
}

packages/toolkit/src/createAction.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
IfVoid,
66
IsAny,
77
} from './tsHelpers'
8+
import { hasMatchFunction } from './tsHelpers'
89
import isPlainObject from './isPlainObject'
910

1011
/**
@@ -293,6 +294,20 @@ export function isAction(action: unknown): action is Action<unknown> {
293294
return isPlainObject(action) && 'type' in action
294295
}
295296

297+
/**
298+
* Returns true if value is an RTK-like action creator, with a static type property and match method.
299+
*/
300+
export function isActionCreator(
301+
action: unknown
302+
): action is BaseActionCreator<unknown, string> & Function {
303+
return (
304+
typeof action === 'function' &&
305+
'type' in action &&
306+
// hasMatchFunction only wants Matchers but I don't see the point in rewriting it
307+
hasMatchFunction(action as any)
308+
)
309+
}
310+
296311
/**
297312
* Returns true if value is an action with a string type and valid Flux Standard Action keys.
298313
*/

packages/toolkit/src/getDefaultMiddleware.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { Middleware, AnyAction } from 'redux'
22
import type { ThunkMiddleware } from 'redux-thunk'
33
import thunkMiddleware from 'redux-thunk'
4+
import type { ActionCreatorInvariantMiddlewareOptions } from './actionCreatorInvariantMiddleware'
5+
import { createActionCreatorInvariantMiddleware } from './actionCreatorInvariantMiddleware'
46
import type { ImmutableStateInvariantMiddlewareOptions } from './immutableStateInvariantMiddleware'
57
/* PROD_START_REMOVE_UMD */
68
import { createImmutableStateInvariantMiddleware } from './immutableStateInvariantMiddleware'
@@ -23,6 +25,7 @@ interface GetDefaultMiddlewareOptions {
2325
thunk?: boolean | ThunkOptions
2426
immutableCheck?: boolean | ImmutableStateInvariantMiddlewareOptions
2527
serializableCheck?: boolean | SerializableStateInvariantMiddlewareOptions
28+
actionCreatorCheck?: boolean | ActionCreatorInvariantMiddlewareOptions
2629
}
2730

2831
export type ThunkMiddlewareFor<
@@ -72,6 +75,7 @@ export function getDefaultMiddleware<
7275
thunk: true
7376
immutableCheck: true
7477
serializableCheck: true
78+
actionCreatorCheck: true
7579
}
7680
>(
7781
options: O = {} as O
@@ -80,6 +84,7 @@ export function getDefaultMiddleware<
8084
thunk = true,
8185
immutableCheck = true,
8286
serializableCheck = true,
87+
actionCreatorCheck = true,
8388
} = options
8489

8590
let middlewareArray = new MiddlewareArray<Middleware[]>()
@@ -120,6 +125,17 @@ export function getDefaultMiddleware<
120125
createSerializableStateInvariantMiddleware(serializableOptions)
121126
)
122127
}
128+
if (actionCreatorCheck) {
129+
let actionCreatorOptions: ActionCreatorInvariantMiddlewareOptions = {}
130+
131+
if (!isBoolean(actionCreatorCheck)) {
132+
actionCreatorOptions = actionCreatorCheck
133+
}
134+
135+
middlewareArray.push(
136+
createActionCreatorInvariantMiddleware(actionCreatorOptions)
137+
)
138+
}
123139
}
124140

125141
return middlewareArray as any

packages/toolkit/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {
4040
createAction,
4141
getType,
4242
isAction,
43+
isActionCreator,
4344
isFSA as isFluxStandardAction,
4445
} from './createAction'
4546
export type {
@@ -78,6 +79,8 @@ export type {
7879
CaseReducerWithPrepare,
7980
SliceActionCreator,
8081
} from './createSlice'
82+
export type { ActionCreatorInvariantMiddlewareOptions } from './actionCreatorInvariantMiddleware'
83+
export { createActionCreatorInvariantMiddleware } from './actionCreatorInvariantMiddleware'
8184
export {
8285
// js
8386
createImmutableStateInvariantMiddleware,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { ActionCreatorInvariantMiddlewareOptions } from '@internal/actionCreatorInvariantMiddleware'
2+
import { getMessage } from '@internal/actionCreatorInvariantMiddleware'
3+
import { createActionCreatorInvariantMiddleware } from '@internal/actionCreatorInvariantMiddleware'
4+
import type { Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'
5+
import { createAction } from '@reduxjs/toolkit'
6+
7+
describe('createActionCreatorInvariantMiddleware', () => {
8+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
9+
10+
afterEach(() => {
11+
consoleSpy.mockClear()
12+
})
13+
afterAll(() => {
14+
consoleSpy.mockRestore()
15+
})
16+
17+
const dummyAction = createAction('anAction')
18+
19+
it('sends the action through the middleware chain', () => {
20+
const next: Dispatch = (action) => ({
21+
...action,
22+
returned: true,
23+
})
24+
const dispatch = createActionCreatorInvariantMiddleware()(
25+
{} as MiddlewareAPI
26+
)(next)
27+
28+
expect(dispatch(dummyAction())).toEqual({
29+
...dummyAction(),
30+
returned: true,
31+
})
32+
})
33+
34+
const makeActionTester = (
35+
options?: ActionCreatorInvariantMiddlewareOptions
36+
) =>
37+
createActionCreatorInvariantMiddleware(options)({} as MiddlewareAPI)(
38+
(action) => action
39+
)
40+
41+
it('logs a warning to console if an action creator is mistakenly dispatched', () => {
42+
const testAction = makeActionTester()
43+
44+
testAction(dummyAction())
45+
46+
expect(consoleSpy).not.toHaveBeenCalled()
47+
48+
testAction(dummyAction)
49+
50+
expect(consoleSpy).toHaveBeenLastCalledWith(getMessage(dummyAction.type))
51+
})
52+
53+
it('allows passing a custom predicate', () => {
54+
let predicateCalled = false
55+
const testAction = makeActionTester({
56+
isActionCreator(action): action is Function {
57+
predicateCalled = true
58+
return false
59+
},
60+
})
61+
testAction(dummyAction())
62+
expect(predicateCalled).toBe(true)
63+
})
64+
})

packages/toolkit/src/tests/getDefaultMiddleware.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import thunk from 'redux-thunk'
1515
import type { ThunkMiddleware } from 'redux-thunk'
1616

1717
import { expectType } from './helpers'
18+
import { BaseActionCreator } from '@internal/createAction'
1819

1920
describe('getDefaultMiddleware', () => {
2021
const ORIGINAL_NODE_ENV = process.env.NODE_ENV
@@ -54,12 +55,19 @@ describe('getDefaultMiddleware', () => {
5455
expect(middleware.length).toBe(defaultMiddleware.length - 1)
5556
})
5657

58+
it('removes the action creator middleware if disabled', () => {
59+
const defaultMiddleware = getDefaultMiddleware()
60+
const middleware = getDefaultMiddleware({ actionCreatorCheck: false })
61+
expect(middleware.length).toBe(defaultMiddleware.length - 1)
62+
})
63+
5764
it('allows passing options to thunk', () => {
5865
const extraArgument = 42 as const
5966
const middleware = getDefaultMiddleware({
6067
thunk: { extraArgument },
6168
immutableCheck: false,
6269
serializableCheck: false,
70+
actionCreatorCheck: false,
6371
})
6472

6573
const m2 = getDefaultMiddleware({
@@ -129,6 +137,7 @@ describe('getDefaultMiddleware', () => {
129137
},
130138
},
131139
serializableCheck: false,
140+
actionCreatorCheck: false,
132141
})
133142

134143
const reducer = () => ({})
@@ -153,6 +162,7 @@ describe('getDefaultMiddleware', () => {
153162
return true
154163
},
155164
},
165+
actionCreatorCheck: false,
156166
})
157167

158168
const reducer = () => ({})
@@ -168,6 +178,33 @@ describe('getDefaultMiddleware', () => {
168178
})
169179
})
170180

181+
it('allows passing options to actionCreatorCheck', () => {
182+
let actionCreatorCheckWasCalled = false
183+
184+
const middleware = getDefaultMiddleware({
185+
thunk: false,
186+
immutableCheck: false,
187+
serializableCheck: false,
188+
actionCreatorCheck: {
189+
isActionCreator: (action: unknown): action is Function => {
190+
actionCreatorCheckWasCalled = true
191+
return false
192+
},
193+
},
194+
})
195+
196+
const reducer = () => ({})
197+
198+
const store = configureStore({
199+
reducer,
200+
middleware,
201+
})
202+
203+
store.dispatch({ type: 'TEST_ACTION' })
204+
205+
expect(actionCreatorCheckWasCalled).toBe(true)
206+
})
207+
171208
describe('MiddlewareArray functionality', () => {
172209
const middleware1: Middleware = () => (next) => (action) => next(action)
173210
const middleware2: Middleware = () => (next) => (action) => next(action)

0 commit comments

Comments
 (0)