How the Redux isPlainObject Utility Validates Actions: A Deep Dive into Source Code
The isPlainObject utility ensures Redux actions are plain JavaScript objects by traversing the prototype chain to verify inheritance from Object.prototype or null, rejecting class instances, arrays, dates, and other non-serializable types.
The isPlainObject utility is a critical validation mechanism in the reduxjs/redux repository that enforces one of Redux's fundamental architectural constraints: every action must be a plain object. Located in src/utils/isPlainObject.ts, this lightweight function serves as the gatekeeper that prevents class instances, arrays, and other object subtypes from entering the dispatch pipeline, ensuring predictable state serialization and time-travel debugging capabilities.
What Defines a Plain Object in Redux
Redux strictly requires that actions be plain objects—specifically, objects created via object literal syntax ({}) or Object.create(null). This excludes class instances, arrays, Date objects, Map and Set instances, and any other object with a modified prototype chain.
The isPlainObject function implements this check by walking the prototype chain until it reaches the terminal null prototype, then comparing the original object's immediate prototype against this terminal value.
How isPlainObject Works Under the Hood
The implementation in src/utils/isPlainObject.ts uses a prototype traversal algorithm to determine object "plainness":
// src/utils/isPlainObject.ts
export default function isPlainObject(obj: any): obj is object {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return (
Object.getPrototypeOf(obj) === proto ||
Object.getPrototypeOf(obj) === null
)
}
The function executes three distinct validation phases:
- Type Guard: Immediately returns
falsefor non-object types andnullvalues - Prototype Traversal: Walks up the prototype chain using
Object.getPrototypeOf()until reaching the terminalnullprototype - Equality Check: Verifies the original object's prototype matches the terminal prototype (or is itself
null)
This algorithm correctly identifies objects created via Object.create(null) as plain objects while rejecting instances of custom classes, which maintain their constructor prototypes in the chain.
Action Validation with isAction
Redux builds upon isPlainObject with the isAction utility in src/utils/isAction.ts to enforce the complete action contract:
// src/utils/isAction.ts
import type { Action } from '../types/actions'
import isPlainObject from './isPlainObject'
export default function isAction(action: unknown): action is Action<string> {
return (
isPlainObject(action) &&
'type' in action &&
typeof (action as Record<'type', unknown>).type === 'string'
)
}
The isAction function performs sequential validation:
- Calls
isPlainObject(action)to ensure the value is a plain object - Checks for the presence of a
typeproperty using theinoperator - Validates that
action.typeis specifically a string type
This dual-layer validation ensures that only properly structured action objects reach the reducer pipeline.
Where Redux Applies isPlainObject Checks
Dispatch Validation in createStore
The primary enforcement point occurs in the dispatch function within src/createStore.ts. Before any action reaches the reducers, Redux validates it using isPlainObject:
// src/createStore.ts (excerpt)
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(
action
)}'. You may need to add middleware to your store setup to handle dispatching other values such as functions or Promises.`
)
}
// Additional validation for action.type follows...
}
This check throws a descriptive error immediately when developers attempt to dispatch class instances, arrays, or functions without appropriate middleware.
State Validation in combineReducers
The combineReducers utility in src/combineReducers.ts also employs isPlainObject to validate that reducer return values are plain objects, preventing accidental mutation of state shapes with non-plain object types.
Practical Examples
Identifying Plain Objects vs. Other Types
import isPlainObject from 'redux/src/utils/isPlainObject'
// True cases - plain objects
console.log(isPlainObject({})) // true
console.log(isPlainObject({ type: 'ADD' })) // true
console.log(isPlainObject(Object.create(null))) // true
// False cases - not plain objects
console.log(isPlainObject([])) // false (array)
console.log(isPlainObject(new Date())) // false (Date instance)
console.log(isPlainObject(new Map())) // false (Map instance)
console.log(isPlainObject(function() {})) // false (function)
console.log(isPlainObject(null)) // false (null)
console.log(isPlainObject('string')) // false (primitive)
// Class instances fail the check
class MyClass {}
console.log(isPlainObject(new MyClass())) // false
Manual Action Validation
import isAction from 'redux/src/utils/isAction'
const validAction = { type: 'INCREMENT', payload: 1 }
const invalidAction1 = { payload: 1 } // missing type
const invalidAction2 = { type: 123 } // type not string
const invalidAction3 = new Map([['type', 'ADD']]) // not plain object
console.log(isAction(validAction)) // true
console.log(isAction(invalidAction1)) // false
console.log(isAction(invalidAction2)) // false
console.log(isAction(invalidAction3)) // false
Catching Dispatch Errors
import { createStore } from 'redux'
const reducer = (state = 0, action) => {
if (action.type === 'INC') return state + 1
return state
}
const store = createStore(reducer)
// This throws an error because functions are not plain objects
try {
store.dispatch(() => ({ type: 'INC' }))
} catch (e) {
console.error(e.message)
// Output: Actions must be plain objects. Instead, the actual type was: 'function'.
// You may need to add middleware to your store setup...
}
Summary
-
The
isPlainObjectutility insrc/utils/isPlainObject.tsvalidates that a value is a plain JavaScript object by traversing its prototype chain to verify inheritance fromObject.prototypeornull. -
Prototype chain traversal distinguishes plain objects from class instances, arrays, dates, and other built-in objects, ensuring Redux actions remain serializable and predictable.
-
The
isActionhelper insrc/utils/isAction.tscombinesisPlainObjectwithtypeproperty validation to enforce the complete Redux action contract. -
Validation occurs at dispatch time in
src/createStore.ts, throwing descriptive errors when non-plain objects reach the store without appropriate middleware. -
State shape protection in
src/combineReducers.tsuses the same utility to ensure reducer return values maintain plain object structure.
Frequently Asked Questions
What is the difference between isPlainObject and isAction in Redux?
isPlainObject is a low-level utility that checks whether a value is a plain JavaScript object by analyzing its prototype chain, returning true only for objects created via {} or Object.create(null). isAction builds upon this foundation in src/utils/isAction.ts by first calling isPlainObject and then verifying that the object contains a type property with a string value, ensuring compliance with the Redux action contract.
Why does Redux reject class instances as actions?
Redux rejects class instances because they fail the isPlainObject check implemented in src/utils/isPlainObject.ts. Class instances maintain a prototype chain that includes the class constructor, not just Object.prototype, causing the prototype traversal algorithm to detect a mismatch. This restriction ensures actions remain serializable and replayable, which is essential for Redux DevTools time-travel debugging and state persistence.
How does isPlainObject handle Object.create(null)?
The isPlainObject utility correctly identifies objects created via Object.create(null) as plain objects because it accepts prototypes of either Object.prototype or null. In src/utils/isPlainObject.ts, the function walks the prototype chain until reaching null, then compares the original object's prototype against this terminal value. Since Object.create(null) has no prototype (strictly null), it satisfies the condition Object.getPrototypeOf(obj) === null, returning true.
Can I dispatch actions without using isPlainObject validation?
You cannot bypass isPlainObject validation in the standard dispatch implementation in src/createStore.ts without modifying the Redux source code. However, you can dispatch non-plain objects by using middleware such as redux-thunk or redux-saga, which intercepts the dispatch process before it reaches the store's validation logic. These middleware packages transform functions or promises into plain objects that satisfy the isPlainObject check by the time they reach the reducers.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →