Reducing Mutable State
Mutable state is the root of most bugs.
Race conditions, stale data, unexpected side effects—these all stem from state that can change unexpectedly. Every piece of mutable state is a variable that might not be what you think it is.
Reducing mutation doesn't mean eliminating it entirely. It means being intentional: mutate when you have to, but default to immutability.
The Problem with Mutation
Shared Mutable State
const user = { name: 'Alice', role: 'admin' };
function downgradeUser(user) {
user.role = 'viewer'; // Mutates the original!
}
function processUser(user) {
// Assumes user is still admin
if (user.role === 'admin') {
// ...
}
}
// Somewhere in the code
downgradeUser(user);
// ... later ...
processUser(user); // Unexpected behavior!
The mutation in one place affects behavior in another. Now imagine this across hundreds of files.
Temporal Coupling
let order = null;
function initializeOrder(items) {
order = { items, status: 'draft' };
}
function submitOrder() {
order.status = 'submitted'; // Assumes initializeOrder was called
}
function getOrderTotal() {
return order.items.reduce(...); // Assumes order exists
}
The functions depend on being called in the right sequence. Call them out of order and things break.
const by Default
In JavaScript/TypeScript, use const unless you need to reassign:
// BAD: Using let when value doesn't change
let name = 'Alice';
let items = [1, 2, 3];
// GOOD: const signals intent
const name = 'Alice';
const items = [1, 2, 3];
Note: const only prevents reassignment, not mutation:
const user = { name: 'Alice' };
user.name = 'Bob'; // This works! Object is mutated, not reassigned.
For true immutability, you need immutable patterns or libraries.
Immutable Update Patterns
Objects: Spread Syntax
// Instead of mutation
user.name = 'Bob';
user.age = 31;
// Use spread for immutable update
const updatedUser = {
...user,
name: 'Bob',
age: 31,
};
Nested Objects
// Deep mutation (dangerous)
user.address.city = 'NYC';
// Immutable deep update
const updatedUser = {
...user,
address: {
...user.address,
city: 'NYC',
},
};
Arrays
// Instead of push (mutates)
items.push(newItem);
// Spread (immutable)
const newItems = [...items, newItem];
// Instead of splice (mutates)
items.splice(index, 1);
// Filter (immutable)
const newItems = items.filter((_, i) => i !== index);
// Instead of sort (mutates)
items.sort();
// Spread then sort
const sorted = [...items].sort();
When Mutation Is Acceptable
Immutability has costs: more objects, more memory, potentially more GC. Mutation is appropriate when:
Local Mutation Within a Function
function processItems(items) {
const results = []; // Mutable, but local
for (const item of items) {
results.push(transform(item)); // Local mutation is fine
}
return results; // Result is immutable to callers
}
The mutation is contained. Callers see only the result.
Performance-Critical Code
// In a hot loop with millions of iterations
function sum(numbers) {
let total = 0; // Mutation for performance
for (let i = 0; i < numbers.length; i++) {
total += numbers[i];
}
return total;
}
Creating millions of intermediate objects would be wasteful. Profile before optimizing, but mutation can be justified.
Builder Pattern
class QueryBuilder {
private query = {};
where(field, value) {
this.query.where = { ...this.query.where, [field]: value };
return this; // Mutation for fluent API
}
orderBy(field) {
this.query.orderBy = field;
return this;
}
build() {
return { ...this.query }; // Return immutable copy
}
}
const query = new QueryBuilder()
.where('status', 'active')
.orderBy('createdAt')
.build();
Immutable by Convention
Without library support, enforce immutability by convention:
Object.freeze for True Immutability
const config = Object.freeze({
apiUrl: 'https://api.example.com',
timeout: 5000,
});
config.timeout = 10000; // Silently fails (or throws in strict mode)
Note: freeze is shallow. Deep freeze requires a utility:
function deepFreeze(obj) {
Object.keys(obj).forEach(key => {
if (typeof obj[key] === 'object' && obj[key] !== null) {
deepFreeze(obj[key]);
}
});
return Object.freeze(obj);
}
TypeScript: Readonly Types
interface User {
readonly id: string;
readonly name: string;
readonly email: string;
}
// Or for all properties
type ImmutableUser = Readonly<User>;
// Compiler prevents mutation
const user: ImmutableUser = { id: '1', name: 'Alice', email: 'a@b.com' };
user.name = 'Bob'; // Error: Cannot assign to 'name' because it is a read-only property
as const
const ROLES = ['admin', 'editor', 'viewer'] as const;
// Type is readonly ['admin', 'editor', 'viewer'], not string[]
ROLES.push('guest'); // Error: Property 'push' does not exist
Immutability Libraries
For complex applications, consider libraries:
Immer
import { produce } from 'immer';
const user = { name: 'Alice', address: { city: 'NYC' } };
const updatedUser = produce(user, draft => {
draft.address.city = 'LA'; // "Mutate" the draft
});
// user unchanged, updatedUser has new address
Immer lets you write mutation-style code but produces immutable results.
Immutable.js
import { Map } from 'immutable';
const user = Map({ name: 'Alice', age: 30 });
const older = user.set('age', 31);
user.get('age'); // 30
older.get('age'); // 31
Different API, but guarantees immutability with structural sharing for performance.
The Benefits
Easier Debugging
When data is immutable, you know it hasn't changed since creation. No need to trace "who modified this?"
Simpler Testing
Immutable functions don't have side effects on inputs. Test input → output, done.
Undo/Redo for Free
Each state is a snapshot. Keep them in an array for history:
const history = [initialState];
function update(newState) {
history.push(newState);
}
function undo() {
history.pop();
return history[history.length - 1];
}
Concurrency Safety
Immutable data can be shared across threads (or async operations) without locks.
Key insight: Mutable state is where bugs hide. Use const by default. Prefer immutable update patterns (spread) over in-place mutation. Contain necessary mutation within function boundaries. Use TypeScript's readonly types or libraries like Immer for enforcement.