Skip to content

Commit 1b39f25

Browse files
kevin940726markerikson
authored andcommitted
Add ignoredPaths option to ignore serializability check (#320)
* Support ignoredSlices in serializableStateInvariantMiddleware * Don't need to check for length * Change to ignoredPaths * Update doc * Check hasIgnoredPaths first to skip checking indexOf
1 parent ac1a673 commit 1b39f25

File tree

4 files changed

+101
-6
lines changed

4 files changed

+101
-6
lines changed

docs/api/otherExports.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,26 @@ Redux Toolkit exports some of its internal utilities, and re-exports additional
1515

1616
Creates an instance of the `serializable-state-invariant` middleware described in [`getDefaultMiddleware`](./getDefaultMiddleware.md).
1717

18-
Accepts an options object with `isSerializable` and `getEntries` parameters. The former, `isSerializable`, will be used to determine if a value is considered serializable or not. If not provided, this defaults to `isPlain`. The latter, `getEntries`, will be used to retrieve nested values. If not provided, `Object.entries` will be used by default.
18+
Accepts a single configuration object parameter, with the following options:
19+
20+
```ts
21+
function createSerializableStateInvariantMiddleware({
22+
// The function to check if a value is considered serializable.
23+
// This function is applied recursively to every value contained in the state.
24+
// Defaults to `isPlain()`.
25+
isSerializable?: (value: any) => boolean
26+
// The function that will be used to retrieve entries from each value.
27+
// If unspecified, `Object.entries` will be used.
28+
// Defaults to `undefined`.
29+
getEntries?: (value: any) => [string, any][]
30+
// An array of action types to ignore when checking for serializability.
31+
// Defaults to []
32+
ignoredActions?: string[]
33+
// An array of dot-separated path strings to ignore when checking for serializability.
34+
// Defaults to []
35+
ignoredPaths?: string[]
36+
})
37+
```
1938

2039
Example:
2140

etc/redux-toolkit.api.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends
130130
}
131131

132132
// @public (undocumented)
133-
export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][]): NonSerializableValue | false;
133+
export function findNonSerializableValue(value: unknown, path?: ReadonlyArray<string>, isSerializable?: (value: unknown) => boolean, getEntries?: (value: unknown) => [string, any][], ignoredPaths?: string[]): NonSerializableValue | false;
134134

135135
// @public
136136
export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddlewareOptions> = {
@@ -177,6 +177,7 @@ export type PrepareAction<P> = ((...args: any[]) => {
177177
export interface SerializableStateInvariantMiddlewareOptions {
178178
getEntries?: (value: any) => [string, any][];
179179
ignoredActions?: string[];
180+
ignoredPaths?: string[];
180181
isSerializable?: (value: any) => boolean;
181182
}
182183

src/serializableStateInvariantMiddleware.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,60 @@ describe('serializableStateInvariantMiddleware', () => {
329329

330330
expect(numTimesCalled).toBeGreaterThan(0)
331331
})
332+
333+
it('should not check serializability for ignored slice names', () => {
334+
const ACTION_TYPE = 'TEST_ACTION'
335+
336+
const initialState = {
337+
a: 0
338+
}
339+
340+
const badValue = new Map()
341+
342+
const reducer: Reducer = (state = initialState, action) => {
343+
switch (action.type) {
344+
case ACTION_TYPE: {
345+
return {
346+
a: badValue,
347+
b: {
348+
c: badValue,
349+
d: badValue
350+
},
351+
e: { f: badValue }
352+
}
353+
}
354+
default:
355+
return state
356+
}
357+
}
358+
359+
const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
360+
{
361+
ignoredPaths: [
362+
// Test for ignoring a single value
363+
'testSlice.a',
364+
// Test for ignoring a single nested value
365+
'testSlice.b.c',
366+
// Test for ignoring an object and its children
367+
'testSlice.e'
368+
]
369+
}
370+
)
371+
372+
const store = configureStore({
373+
reducer: {
374+
testSlice: reducer
375+
},
376+
middleware: [serializableStateInvariantMiddleware]
377+
})
378+
379+
store.dispatch({ type: ACTION_TYPE })
380+
381+
// testSlice.b.d was not covered in ignoredPaths, so will still log the error
382+
expect(getLog().log).toMatchInlineSnapshot(`
383+
"A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {}
384+
Take a look at the reducer(s) handling this action type: TEST_ACTION.
385+
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
386+
`)
387+
})
332388
})

src/serializableStateInvariantMiddleware.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export function findNonSerializableValue(
3434
value: unknown,
3535
path: ReadonlyArray<string> = [],
3636
isSerializable: (value: unknown) => boolean = isPlain,
37-
getEntries?: (value: unknown) => [string, any][]
37+
getEntries?: (value: unknown) => [string, any][],
38+
ignoredPaths: string[] = []
3839
): NonSerializableValue | false {
3940
let foundNestedSerializable: NonSerializableValue | false
4041

@@ -51,9 +52,15 @@ export function findNonSerializableValue(
5152

5253
const entries = getEntries != null ? getEntries(value) : Object.entries(value)
5354

55+
const hasIgnoredPaths = ignoredPaths.length > 0
56+
5457
for (const [property, nestedValue] of entries) {
5558
const nestedPath = path.concat(property)
5659

60+
if (hasIgnoredPaths && ignoredPaths.indexOf(nestedPath.join('.')) >= 0) {
61+
continue
62+
}
63+
5764
if (!isSerializable(nestedValue)) {
5865
return {
5966
keyPath: nestedPath.join('.'),
@@ -66,7 +73,8 @@ export function findNonSerializableValue(
6673
nestedValue,
6774
nestedPath,
6875
isSerializable,
69-
getEntries
76+
getEntries,
77+
ignoredPaths
7078
)
7179

7280
if (foundNestedSerializable) {
@@ -101,6 +109,11 @@ export interface SerializableStateInvariantMiddlewareOptions {
101109
* An array of action types to ignore when checking for serializability, Defaults to []
102110
*/
103111
ignoredActions?: string[]
112+
113+
/**
114+
* An array of dot-separated path strings to ignore when checking for serializability, Defaults to []
115+
*/
116+
ignoredPaths?: string[]
104117
}
105118

106119
/**
@@ -115,7 +128,12 @@ export interface SerializableStateInvariantMiddlewareOptions {
115128
export function createSerializableStateInvariantMiddleware(
116129
options: SerializableStateInvariantMiddlewareOptions = {}
117130
): Middleware {
118-
const { isSerializable = isPlain, getEntries, ignoredActions = [] } = options
131+
const {
132+
isSerializable = isPlain,
133+
getEntries,
134+
ignoredActions = [],
135+
ignoredPaths = []
136+
} = options
119137

120138
return storeAPI => next => action => {
121139
if (ignoredActions.length && ignoredActions.indexOf(action.type) !== -1) {
@@ -149,7 +167,8 @@ export function createSerializableStateInvariantMiddleware(
149167
state,
150168
[],
151169
isSerializable,
152-
getEntries
170+
getEntries,
171+
ignoredPaths
153172
)
154173

155174
if (foundStateNonSerializableValue) {

0 commit comments

Comments
 (0)