use-mutable-state-hook
v1.0.0
Published
React hook for easily deeply modifying state
Maintainers
Readme
📦 use-mutable-state-hook
📖 Table of Contents
- Introduction
- Installation
- Usage
- API
- Examples
- Caveats
- Performance Considerations
- Use Cases & Considerations
- Why Use This?
- License
🚀 Introduction
use-mutable-state-hook is a lightweight and efficient React hook that enables deep state mutation with automatic reactivity. With this hook, you can modify deeply nested objects without needing to spread state manually or call additional update functions—thanks to JavaScript Proxies.
TL;DR you can condense code like this:
import { useState } from 'react';
const [state, setState] = useState({
level1: { level2: { level3: { level4: { count: 0 } } } },
});
const increment = () => {
setState((prev) => ({
...prev,
level1: {
...prev.level1,
level2: {
...prev.level1.level2,
level3: {
...prev.level1.level2.level3,
level4: {
...prev.level1.level2.level3.level4,
count: prev.level1.level2.level3.level4.count + 1,
},
},
},
},
}));
};
return (
<div>
<h3>Count: {state.level1.level2.level3.level4.count}</h3>
<button onClick={increment}>Increment</button>
</div>
);with:
import { useMutableState } from 'use-mutable-state-hook';
const MyComponent = () => {
const state = useMutableState({
level1: { level2: { level3: { level4: { count: 0 } } } },
});
return (
<div>
<h3>Count: {state.level1.level2.level3.level4.count}</h3>
<button onClick={() => state.level1.level2.level3.level4.count++}>Increment</button>
</div>
);
};✅ No Spreading – Modify deeply nested values directly ✅ Automatic Reactivity – No need to call setState manually ✅ Simplifies State Updates – Works with objects & arrays seamlessly
📦 Installation
Install the package via npm or yarn:
bun add use-mutable-state-hookor
pnpm add use-mutable-state-hookor
npm install use-mutable-state-hookor
yarn add use-mutable-state-hook💡 Usage
import { useMutableState } from 'use-mutable-state-hook';
const MyComponent = () => {
const state = useMutableState({
count: 0,
nested: { value: { deep: 42 } },
items: [1, 2, 3],
});
return (
<div>
<h3>Count: {state.count}</h3>
<button onClick={() => state.count++}>Increment Count</button>
<h3>Nested Value: {state.nested.value.deep}</h3>
<button onClick={() => state.nested.value.deep++}>Increment Deep Value</button>
</div>
);
};🛠 API
useMutableState<T>(initialState: T): ProxyState<T>
Creates a deeply reactive proxy state that triggers re-renders upon mutation.
Parameters
initialState: T– The initial state object to be used.
Returns
ProxyState<T>– A proxy object with reactive updates and a.toObject()method to retrieve the plain object state.
📌 Examples
🔹 Modifying State Without Spreading
const state = useMutableState({
user: { name: 'Alice', details: { age: 25 } },
});
state.user.details.age++; // UI re-renders automatically🔹 Using toObject() for Comparison or Sharing
Once you are done with your state object and you need to send it to another component or consume the value, you can use the toObject() method.
const pojo = state.toObject();
console.log(pojo); // Outputs the plain object version of state🔹 Tracking Changes in Effects
useEffect(() => {
console.log('State changed:', state.toObject());
}, [state]); // The proxy reference updates on every mutation.⚠️ Caveats
Proxies Change on Every Render:
- Since a new proxy reference is created on each state update, using
useMemowith[state]will work as expected. - However, be cautious when passing the proxy as a dependency where identity comparisons matter.
- Since a new proxy reference is created on each state update, using
toObject()Always Returns a Fresh Deep Clone:- The returned object is fully detached from the proxy and can be safely mutated without affecting the original state.
RegExp Objects Are Not Deep Cloned:
- If your state includes
RegExpobjects, they will remain the same reference inside the state.
- If your state includes
🚀 Performance Considerations
- State mutations trigger updates efficiently.
- Instead of deep-cloning state on every update, it uses in-place mutation while replacing the proxy reference.
- Arrays are mutable, but costly operations (e.g.,
splice,sort) trigger full re-renders.- Avoid unnecessary array reassignments; prefer push/pop/filter to minimize reactivity overhead.
- For performance-sensitive applications, avoid excessive deep mutations.
- If you frequently modify deeply nested structures, consider whether immutability (e.g., Redux, Zustand) may be a better fit.
🎯 Use Cases & Considerations
When to Use:
✔️ Forms & UI State: Easily manage deeply nested form state without complex state updates.
✔️ Local Component State: Works well for localized UI-driven state that doesn’t need to be stored globally.
✔️ Dynamic Object Modifications: Perfect when working with deeply nested structures that require real-time updates.
When to Avoid:
❌ Large-Scale Global State Management: Proxies can introduce unnecessary complexity when scaling state across multiple components.
❌ Heavy Computation-Based State: If state calculations need to be memoized or derived efficiently, use useState with controlled updates instead.
❌ Highly Immutable State Requirements: If strict immutability is necessary (e.g., undo/redo features, history tracking), this approach may not be ideal.
❓ Why Use This?
- No need for spread operators – Modify deeply nested objects directly.
- No external dependencies – Uses built-in JavaScript Proxies.
- Works with arrays & objects – Mutate lists without extra steps.
- Automatic Reactivity – The proxy updates and triggers re-renders without manual state setters.
📜 License
This project is licensed under the MIT License.
