UNPKG

6.61 kBPlain TextView Raw
1import isPlainObject from './isPlainObject'
2import type { Middleware } from 'redux'
3import { getTimeMeasureUtils } from './utils'
4
5/**
6 * Returns true if the passed value is "plain", i.e. a value that is either
7 * directly JSON-serializable (boolean, number, string, array, plain object)
8 * or `undefined`.
9 *
10 * @param val The value to check.
11 *
12 * @public
13 */
14export function isPlain(val: any) {
15 const type = typeof val
16 return (
17 type === 'undefined' ||
18 val === null ||
19 type === 'string' ||
20 type === 'boolean' ||
21 type === 'number' ||
22 Array.isArray(val) ||
23 isPlainObject(val)
24 )
25}
26
27interface NonSerializableValue {
28 keyPath: string
29 value: unknown
30}
31
32/**
33 * @public
34 */
35export function findNonSerializableValue(
36 value: unknown,
37 path: string = '',
38 isSerializable: (value: unknown) => boolean = isPlain,
39 getEntries?: (value: unknown) => [string, any][],
40 ignoredPaths: readonly string[] = []
41): NonSerializableValue | false {
42 let foundNestedSerializable: NonSerializableValue | false
43
44 if (!isSerializable(value)) {
45 return {
46 keyPath: path || '<root>',
47 value: value,
48 }
49 }
50
51 if (typeof value !== 'object' || value === null) {
52 return false
53 }
54
55 const entries = getEntries != null ? getEntries(value) : Object.entries(value)
56
57 const hasIgnoredPaths = ignoredPaths.length > 0
58
59 for (const [key, nestedValue] of entries) {
60 const nestedPath = path ? path + '.' + key : key
61
62 if (hasIgnoredPaths && ignoredPaths.indexOf(nestedPath) >= 0) {
63 continue
64 }
65
66 if (!isSerializable(nestedValue)) {
67 return {
68 keyPath: nestedPath,
69 value: nestedValue,
70 }
71 }
72
73 if (typeof nestedValue === 'object') {
74 foundNestedSerializable = findNonSerializableValue(
75 nestedValue,
76 nestedPath,
77 isSerializable,
78 getEntries,
79 ignoredPaths
80 )
81
82 if (foundNestedSerializable) {
83 return foundNestedSerializable
84 }
85 }
86 }
87
88 return false
89}
90
91/**
92 * Options for `createSerializableStateInvariantMiddleware()`.
93 *
94 * @public
95 */
96export interface SerializableStateInvariantMiddlewareOptions {
97 /**
98 * The function to check if a value is considered serializable. This
99 * function is applied recursively to every value contained in the
100 * state. Defaults to `isPlain()`.
101 */
102 isSerializable?: (value: any) => boolean
103 /**
104 * The function that will be used to retrieve entries from each
105 * value. If unspecified, `Object.entries` will be used. Defaults
106 * to `undefined`.
107 */
108 getEntries?: (value: any) => [string, any][]
109
110 /**
111 * An array of action types to ignore when checking for serializability.
112 * Defaults to []
113 */
114 ignoredActions?: string[]
115
116 /**
117 * An array of dot-separated path strings to ignore when checking
118 * for serializability, Defaults to ['meta.arg', 'meta.baseQueryMeta']
119 */
120 ignoredActionPaths?: string[]
121
122 /**
123 * An array of dot-separated path strings to ignore when checking
124 * for serializability, Defaults to []
125 */
126 ignoredPaths?: string[]
127 /**
128 * Execution time warning threshold. If the middleware takes longer
129 * than `warnAfter` ms, a warning will be displayed in the console.
130 * Defaults to 32ms.
131 */
132 warnAfter?: number
133
134 /**
135 * Opt out of checking state. When set to `true`, other state-related params will be ignored.
136 */
137 ignoreState?: boolean
138
139 /**
140 * Opt out of checking actions. When set to `true`, other action-related params will be ignored.
141 */
142 ignoreActions?: boolean
143}
144
145/**
146 * Creates a middleware that, after every state change, checks if the new
147 * state is serializable. If a non-serializable value is found within the
148 * state, an error is printed to the console.
149 *
150 * @param options Middleware options.
151 *
152 * @public
153 */
154export function createSerializableStateInvariantMiddleware(
155 options: SerializableStateInvariantMiddlewareOptions = {}
156): Middleware {
157 if (process.env.NODE_ENV === 'production') {
158 return () => (next) => (action) => next(action)
159 }
160 const {
161 isSerializable = isPlain,
162 getEntries,
163 ignoredActions = [],
164 ignoredActionPaths = ['meta.arg', 'meta.baseQueryMeta'],
165 ignoredPaths = [],
166 warnAfter = 32,
167 ignoreState = false,
168 ignoreActions = false,
169 } = options
170
171 return (storeAPI) => (next) => (action) => {
172 const result = next(action)
173
174 const measureUtils = getTimeMeasureUtils(
175 warnAfter,
176 'SerializableStateInvariantMiddleware'
177 )
178
179 if (
180 !ignoreActions &&
181 !(ignoredActions.length && ignoredActions.indexOf(action.type) !== -1)
182 ) {
183 measureUtils.measureTime(() => {
184 const foundActionNonSerializableValue = findNonSerializableValue(
185 action,
186 '',
187 isSerializable,
188 getEntries,
189 ignoredActionPaths
190 )
191
192 if (foundActionNonSerializableValue) {
193 const { keyPath, value } = foundActionNonSerializableValue
194
195 console.error(
196 `A non-serializable value was detected in an action, in the path: \`${keyPath}\`. Value:`,
197 value,
198 '\nTake a look at the logic that dispatched this action: ',
199 action,
200 '\n(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)',
201 '\n(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)'
202 )
203 }
204 })
205 }
206
207 if (!ignoreState) {
208 measureUtils.measureTime(() => {
209 const state = storeAPI.getState()
210
211 const foundStateNonSerializableValue = findNonSerializableValue(
212 state,
213 '',
214 isSerializable,
215 getEntries,
216 ignoredPaths
217 )
218
219 if (foundStateNonSerializableValue) {
220 const { keyPath, value } = foundStateNonSerializableValue
221
222 console.error(
223 `A non-serializable value was detected in the state, in the path: \`${keyPath}\`. Value:`,
224 value,
225 `
226Take a look at the reducer(s) handling this action type: ${action.type}.
227(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)`
228 )
229 }
230 })
231
232 measureUtils.warnIfExceeded()
233 }
234
235 return result
236 }
237}