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:

  1. Type Guard: Immediately returns false for non-object types and null values
  2. Prototype Traversal: Walks up the prototype chain using Object.getPrototypeOf() until reaching the terminal null prototype
  3. 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 type property using the in operator
  • Validates that action.type is 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 isPlainObject utility in src/utils/isPlainObject.ts validates that a value is a plain JavaScript object by traversing its prototype chain to verify inheritance from Object.prototype or null.

  • Prototype chain traversal distinguishes plain objects from class instances, arrays, dates, and other built-in objects, ensuring Redux actions remain serializable and predictable.

  • The isAction helper in src/utils/isAction.ts combines isPlainObject with type property 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.ts uses 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →