Initializing Enclave...

How to Fix TypeError: Cannot Assign to Read Only Property of Object in React Props

Bug Severity: CRITICAL | UX/User Impact: SEVERE — component silently breaks or hard-crashes depending on strict mode | Time to Fix: 5 mins

TL;DR

  • What broke: A child component is directly writing to a prop (e.g., props.user.name = 'foo'). Props are frozen in React's strict mode — this throws immediately.
  • How to fix it: Copy the prop into local useState or a new object before mutating. Never write to the props reference directly.
  • Use our Client-Side Sandbox below to auto-refactor this — paste your component and get corrected code without sending your data anywhere.

The Incident (What does the error mean?)

Raw error thrown at runtime:

TypeError: Cannot assign to read only property 'name' of object '#<Object>'
    at ProfileCard (ProfileCard.jsx:12:18)
    at updateFunctionComponent
    at performUnitOfWork

React's reconciler passes props as a sealed, frozen object. In development mode with React.StrictMode enabled, this object is explicitly Object.freeze()'d. Any direct write — props.user.name = 'updated' — throws immediately. In production without strict mode, the write silently fails (non-strict JS just ignores the assignment), which is arguably worse: your UI shows stale data with no error boundary triggered, no stack trace, no alert.


The Attack Vector / Blast Radius

This isn't just a style violation. The blast radius:

  • Silent data corruption in production. Without strict mode, the mutation fails silently. The component re-renders with whatever state it had before. Users see stale or inconsistent UI. Support tickets pile up with no reproducible error.
  • Breaks unidirectional data flow. The parent component that owns the prop has no idea the child tried to change it. Your state management layer (Redux, Zustand, Context) is now out of sync with what the DOM shows.
  • Cascading re-render failures. If the mutated prop is used downstream by sibling components via shared context, those components never receive the update signal. You end up with a split-brain UI.
  • Breaks React DevTools time-travel debugging. Snapshots are corrupted because the historical prop object was mutated in place.

The deeper problem: engineers who write this pattern usually do it in multiple places. One audit typically surfaces 5–15 instances across a codebase.


How to Fix It (The Solution)

Basic Fix — Copy into local state

If the component needs to edit the value, it needs to own it via useState.

- function ProfileCard({ user }) {
-   const handleEdit = () => {
-     user.name = 'New Name'; // ILLEGAL — direct prop mutation
-   };
-   return <div onClick={handleEdit}>{user.name}</div>;
- }

+ function ProfileCard({ user }) {
+   const [localName, setLocalName] = React.useState(user.name);
+   const handleEdit = () => {
+     setLocalName('New Name'); // Correct — local state owns the value
+   };
+   return <div onClick={handleEdit}>{localName}</div>;
+ }

Enterprise Best Practice — Lift state up + callback prop

If the parent needs to know about the change (almost always the case), lift the state and pass a setter callback down.

- function ProfileCard({ user }) {
-   user.name = editedName; // Mutating parent's object directly
- }

+ // Parent owns the state
+ function ParentPage() {
+   const [user, setUser] = React.useState({ name: 'Alice', id: 1 });
+   const handleNameChange = (newName) => {
+     setUser(prev => ({ ...prev, name: newName })); // Immutable update
+   };
+   return <ProfileCard user={user} onNameChange={handleNameChange} />;
+ }
+
+ // Child calls the callback — never touches props directly
+ function ProfileCard({ user, onNameChange }) {
+   return (
+     <div onClick={() => onNameChange('New Name')}>{user.name}</div>
+   );
+ }

For deeply nested objects, use a library like immer to produce immutable updates without manual spreading:

- state.user.address.city = 'Austin'; // Mutation

+ import produce from 'immer';
+ const nextState = produce(state, draft => {
+   draft.user.address.city = 'Austin'; // Safe — immer handles immutability
+ });

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your component trees, user schema shapes, and internal prop structures. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing component into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

Don't rely on runtime errors to catch this. Shift left:

1. ESLint — react/no-direct-mutation-state + no-param-reassign

Add to .eslintrc:

{
  "rules": {
    "no-param-reassign": ["error", {
      "props": true,
      "ignorePropertyModificationsFor": []
    }]
  }
}

no-param-reassign with props: true will flag any write to a function parameter's properties — which is exactly what prop mutation is.

2. TypeScript — Readonly<T> on all prop interfaces

// Enforce at compile time — no runtime cost
interface ProfileCardProps {
  readonly user: Readonly<{
    name: string;
    id: number;
  }>;
}

The compiler rejects the mutation before the code ships.

3. CI gate — block merges on lint failure

In your GitHub Actions workflow:

- name: Lint Check
  run: npx eslint src/ --max-warnings=0

--max-warnings=0 ensures prop mutation warnings are treated as hard errors and block the PR merge.

4. React Strict Mode in all environments

Keep <React.StrictMode> wrapping your app root in both development and staging. It surfaces these mutations as hard throws instead of silent failures, making them impossible to miss during QA.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →