This error occurs in strict mode when attempting to modify a property that has been marked as non-writable, such as frozen objects, React props, or properties defined with Object.freeze() or Object.defineProperty().
This TypeError occurs exclusively in JavaScript's strict mode when code attempts to modify a property that has been explicitly marked as read-only or non-writable. In React applications, this commonly happens when developers try to mutate props directly, modify frozen objects, or change properties that were created with Object.freeze() or Object.defineProperty() with writable: false. Strict mode enforces immutability rules that would otherwise fail silently in non-strict code. React's ecosystem heavily relies on immutability principles, making this error particularly common when developers accidentally violate these patterns. The error serves as a protective mechanism to catch unintended mutations that could lead to hard-to-debug issues. In React, props are intentionally immutable - they're snapshots of data at a specific point in time. When this error appears, it's usually indicating that code is attempting to break React's unidirectional data flow by modifying data that should remain constant throughout a component's lifecycle.
Examine the error stack trace to find the exact line and property causing the issue:
// Error will show something like:
// TypeError: Cannot assign to read-only property 'name' of object '#<Object>'
// Common culprits:
function MyComponent(props) {
props.value = 'new value'; // ❌ Props are read-only
return <div>{props.value}</div>;
}Check if you're modifying:
- React props directly
- Objects/arrays from Object.freeze()
- Properties with writable: false
- useRef.current in TypeScript
In React, never mutate props. Use state and callbacks instead:
// ❌ Wrong - mutating props
function Counter(props) {
props.count = props.count + 1;
return <div>{props.count}</div>;
}
// ✅ Correct - use state for mutable values
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
const increment = () => {
setCount(count + 1); // Proper state update
};
return (
<div>
<p>{count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}
// ✅ Correct - callback to parent for prop changes
function ChildComponent({ value, onValueChange }) {
return (
<input
value={value}
onChange={(e) => onValueChange(e.target.value)}
/>
);
}If you need to modify frozen data structures, clone them first:
// ❌ Wrong - sorting frozen array
function SortedList({ items }) {
const sorted = items.sort(); // Fails if items is frozen
return <ul>{sorted.map(item => <li key={item}>{item}</li>)}</ul>;
}
// ✅ Correct - create copy before sorting
function SortedList({ items }) {
const sorted = [...items].sort(); // Spread creates new array
return <ul>{sorted.map(item => <li key={item}>{item}</li>)}</ul>;
}
// For objects:
const frozenUser = Object.freeze({ name: 'John', age: 30 });
// ❌ Wrong
frozenUser.age = 31;
// ✅ Correct - create new object
const updatedUser = { ...frozenUser, age: 31 };
// Or use Object.assign
const updatedUser2 = Object.assign({}, frozenUser, { age: 31 });When using useRef with TypeScript, include null in the type to make current mutable:
// ❌ Wrong - current is readonly
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current = document.getElementById('input') as HTMLInputElement;
// Error: Cannot assign to 'current' because it is a read-only property
// ✅ Correct - union type with null makes it mutable
const inputRef = useRef<HTMLInputElement | null>(null);
inputRef.current = document.getElementById('input') as HTMLInputElement;
// For callbacks:
function MyComponent() {
const callbackRef = useRef<(() => void) | null>(null);
useEffect(() => {
callbackRef.current = () => {
console.log('Callback executed');
};
}, []);
return <button onClick={() => callbackRef.current?.()}>Click</button>;
}When using libraries like Immer or Immutable.js with React Hook Form or similar libraries:
// react-hook-form example with Immer
import { useForm } from 'react-hook-form';
import produce from 'immer';
function FormComponent() {
const { register, handleSubmit, setValue } = useForm();
// ❌ Wrong - passing frozen object to onChange
const handleChange = (frozenData) => {
setValue('field', frozenData); // Fails with immutable data
};
// ✅ Correct - convert to plain object first
const handleChange = (frozenData) => {
const plainData = JSON.parse(JSON.stringify(frozenData));
setValue('field', plainData);
};
// Or use Immer's produce correctly
const updateData = (data) => {
const newData = produce(data, draft => {
draft.field = 'new value'; // Immer handles immutability
});
setValue('field', newData);
};
return <form onSubmit={handleSubmit(onSubmit)}>...</form>;
}Check if strict mode is necessary for your use case:
// Explicit strict mode
"use strict";
// ES6 modules are automatically in strict mode
import React from 'react';
// React.StrictMode in development
function App() {
return (
<React.StrictMode>
<MyComponent />
</React.StrictMode>
);
}React.StrictMode enables additional checks but doesn't cause this specific error. The TypeError comes from JavaScript's strict mode, which is enabled by:
- ES6 modules (import/export)
- "use strict" directive
- Class bodies (always strict)
- Production builds that transpile with strict mode
If the error only appears in production, ensure your development environment matches production's strict mode settings.
Deep Freeze vs Shallow Freeze
Object.freeze() only performs a shallow freeze - nested objects remain mutable:
const user = Object.freeze({
name: 'John',
address: { city: 'NYC' }
});
user.name = 'Jane'; // ❌ Error in strict mode
user.address.city = 'LA'; // ✅ Works - nested object not frozen
// Deep freeze utility
function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null
&& (typeof obj[prop] === "object" || typeof obj[prop] === "function")
&& !Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}React StrictMode vs JavaScript Strict Mode
These are different concepts:
- React.StrictMode: Development helper that double-invokes effects to catch side effects
- JavaScript strict mode: Language feature that catches coding mistakes and "unsafe" actions
Performance Considerations
Object.freeze() has minimal performance impact but prevents optimizations that rely on object identity. In React:
- Frozen props can prevent unnecessary re-renders (referential equality)
- But they prevent in-place optimizations some libraries rely on
- Use judiciously - not all data needs to be frozen
ESLint Rules
Prevent these errors at compile time:
{
"rules": {
"react/no-direct-mutation-state": "error",
"@typescript-eslint/prefer-readonly": "warn",
"functional/no-let": "warn",
"functional/immutable-data": "error"
}
}Redux DevTools and Frozen State
Redux DevTools may freeze state to detect mutations. If you see this error when DevTools are open but not otherwise, you're likely mutating Redux state:
// ❌ Wrong - mutating state
function reducer(state, action) {
state.count++; // Error with DevTools
return state;
}
// ✅ Correct - return new state
function reducer(state, action) {
return { ...state, count: state.count + 1 };
}React Hook useCallback has a missing dependency: 'variable'. Either include it or remove the dependency array react-hooks/exhaustive-deps
React Hook useCallback has a missing dependency
Cannot use private fields in class components without TS support
Cannot use private fields in class components without TS support
Cannot destructure property 'xxx' of 'undefined'
Cannot destructure property of undefined when accessing props
useNavigate() may be used only in the context of a <Router> component.
useNavigate() may be used only in the context of a Router component
Cannot find module or its corresponding type declarations
How to fix "Cannot find module or type declarations" in Vite