Skip to content

Commit

Permalink
Add ignoredPaths option to ignore serializability check (#320)
Browse files Browse the repository at this point in the history
* Support ignoredSlices in serializableStateInvariantMiddleware

* Don't need to check for length

* Change to ignoredPaths

* Update doc

* Check hasIgnoredPaths first to skip checking indexOf
  • Loading branch information
kevin940726 authored and markerikson committed Jan 19, 2020
1 parent ac1a673 commit 1b39f25
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 6 deletions.
21 changes: 20 additions & 1 deletion docs/api/otherExports.md
Expand Up @@ -15,7 +15,26 @@ Redux Toolkit exports some of its internal utilities, and re-exports additional

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

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.
Accepts a single configuration object parameter, with the following options:

```ts
function createSerializableStateInvariantMiddleware({
// The function to check if a value is considered serializable.
// This function is applied recursively to every value contained in the state.
// Defaults to `isPlain()`.
isSerializable?: (value: any) => boolean
// The function that will be used to retrieve entries from each value.
// If unspecified, `Object.entries` will be used.
// Defaults to `undefined`.
getEntries?: (value: any) => [string, any][]
// An array of action types to ignore when checking for serializability.
// Defaults to []
ignoredActions?: string[]
// An array of dot-separated path strings to ignore when checking for serializability.
// Defaults to []
ignoredPaths?: string[]
})
```

Example:

Expand Down
3 changes: 2 additions & 1 deletion etc/redux-toolkit.api.md
Expand Up @@ -130,7 +130,7 @@ export interface EnhancedStore<S = any, A extends Action = AnyAction, M extends
}

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

// @public
export function getDefaultMiddleware<S = any, O extends Partial<GetDefaultMiddlewareOptions> = {
Expand Down Expand Up @@ -177,6 +177,7 @@ export type PrepareAction<P> = ((...args: any[]) => {
export interface SerializableStateInvariantMiddlewareOptions {
getEntries?: (value: any) => [string, any][];
ignoredActions?: string[];
ignoredPaths?: string[];
isSerializable?: (value: any) => boolean;
}

Expand Down
56 changes: 56 additions & 0 deletions src/serializableStateInvariantMiddleware.test.ts
Expand Up @@ -329,4 +329,60 @@ describe('serializableStateInvariantMiddleware', () => {

expect(numTimesCalled).toBeGreaterThan(0)
})

it('should not check serializability for ignored slice names', () => {
const ACTION_TYPE = 'TEST_ACTION'

const initialState = {
a: 0
}

const badValue = new Map()

const reducer: Reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE: {
return {
a: badValue,
b: {
c: badValue,
d: badValue
},
e: { f: badValue }
}
}
default:
return state
}
}

const serializableStateInvariantMiddleware = createSerializableStateInvariantMiddleware(
{
ignoredPaths: [
// Test for ignoring a single value
'testSlice.a',
// Test for ignoring a single nested value
'testSlice.b.c',
// Test for ignoring an object and its children
'testSlice.e'
]
}
)

const store = configureStore({
reducer: {
testSlice: reducer
},
middleware: [serializableStateInvariantMiddleware]
})

store.dispatch({ type: ACTION_TYPE })

// testSlice.b.d was not covered in ignoredPaths, so will still log the error
expect(getLog().log).toMatchInlineSnapshot(`
"A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {}
Take a look at the reducer(s) handling this action type: TEST_ACTION.
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
`)
})
})
27 changes: 23 additions & 4 deletions src/serializableStateInvariantMiddleware.ts
Expand Up @@ -34,7 +34,8 @@ export function findNonSerializableValue(
value: unknown,
path: ReadonlyArray<string> = [],
isSerializable: (value: unknown) => boolean = isPlain,
getEntries?: (value: unknown) => [string, any][]
getEntries?: (value: unknown) => [string, any][],
ignoredPaths: string[] = []
): NonSerializableValue | false {
let foundNestedSerializable: NonSerializableValue | false

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

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

const hasIgnoredPaths = ignoredPaths.length > 0

for (const [property, nestedValue] of entries) {
const nestedPath = path.concat(property)

if (hasIgnoredPaths && ignoredPaths.indexOf(nestedPath.join('.')) >= 0) {
continue
}

if (!isSerializable(nestedValue)) {
return {
keyPath: nestedPath.join('.'),
Expand All @@ -66,7 +73,8 @@ export function findNonSerializableValue(
nestedValue,
nestedPath,
isSerializable,
getEntries
getEntries,
ignoredPaths
)

if (foundNestedSerializable) {
Expand Down Expand Up @@ -101,6 +109,11 @@ export interface SerializableStateInvariantMiddlewareOptions {
* An array of action types to ignore when checking for serializability, Defaults to []
*/
ignoredActions?: string[]

/**
* An array of dot-separated path strings to ignore when checking for serializability, Defaults to []
*/
ignoredPaths?: string[]
}

/**
Expand All @@ -115,7 +128,12 @@ export interface SerializableStateInvariantMiddlewareOptions {
export function createSerializableStateInvariantMiddleware(
options: SerializableStateInvariantMiddlewareOptions = {}
): Middleware {
const { isSerializable = isPlain, getEntries, ignoredActions = [] } = options
const {
isSerializable = isPlain,
getEntries,
ignoredActions = [],
ignoredPaths = []
} = options

return storeAPI => next => action => {
if (ignoredActions.length && ignoredActions.indexOf(action.type) !== -1) {
Expand Down Expand Up @@ -149,7 +167,8 @@ export function createSerializableStateInvariantMiddleware(
state,
[],
isSerializable,
getEntries
getEntries,
ignoredPaths
)

if (foundStateNonSerializableValue) {
Expand Down

0 comments on commit 1b39f25

Please sign in to comment.