morph-state
is a comprehensive mutable state management library for React built using TypeScript. It offers three distinct ways to manage state efficiently: component-wise, global state outside of components, and context-based global state. By leveraging proxy objects, it allows for fine-grained mutations at any nested property level while ensuring optimal performance.
useState
but allows deeper property mutations.<MorphStateProvider>
and useMorphState
hooks.You can install morph-state
using your preferred package manager:
npm install morph-state
yarn add morph-state
pnpm add morph-state
Initialize with an optional initial state and an optional change callback:
import React from 'react';
import { useMutableState } from 'morph-state';
function App() {
const state = useMutableState({ count: 0 }, (value, { field, update, cancel }) => {
if (value < 0) cancel();
else if (value > 10) update(10);
console.log(`Changed ${field} to ${value}`);
});
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.count--}>Decrement</button>
</div>
);
}
Create and use state outside React components:
import { createStore, valueOf } from 'morph-state';
const initialState = {
name: "Shridhar",
age: 10,
address: { stateCode: "TN", city: "Chennai" }
};
const callback = (value, { field, update, cancel }) => {
console.log(`State change at ${field} to ${value}`);
if (field === 'age' && value < 0) cancel();
};
const store = createStore(initialState, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true,
onChange: callback
});
// Accessing state outside components
console.log(valueOf(store.state.name)); // Outputs: Shridhar
import React from 'react';
import { createStore, createHook } from 'morph-state';
const store = createStore({ count: 0 });
const useCount = createHook(store);
function Counter() {
const state = useCount();
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => state.count++}>Increment</button>
</div>
);
}
const useAddress = createHook(store, (state) => state.address);
function AddressInfo() {
const address = useAddress();
return (
<div>
<p>City: {address.city}</p>
<p>State: {address.stateCode}</p>
</div>
);
}
import React, { useEffect } from 'react';
import { createStore, createHook, valueOf } from 'morph-state';
// Create the store outside any React component
const store = createStore({ appState: { sharedValue: 0 } }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true,
interceptObjects: true
});
// External function to modify store value
function modifyStoreValue(newValue) {
store.state.appState.sharedValue = newValue;
}
// Example set interval which changes the value every 2 seconds
const interval = setInterval(() => {
modifyStoreValue(Math.floor(Math.random() * 100));
}, 2000);
// clearInterval(interval); // Clear it when not needed
const useAppState = createHook(store, (state) => state.appState);
function SharedValueComponent() {
// The `appState` would be a proxy object as `interceptObjects` config is set to `true`
// The `appState.sharedValue` would also be a proxy object as `interceptValues` config is set to `true`
// You cannot directly use this proxy object and hence you need `valueOf` helper function to get the actual value
// As `appState` is a proxy object, you can assign/mutate any property values and it would be available throughout your page
const appState = useSharedValue();
return (
<div>
<p>Shared Value: {valueOf(appState.sharedValue)}</p>
<button onClick={() => appState.sharedValue = valueOf(sharedValue) + 200}>Increment Shared Value by 200</button>
</div>
);
}
export default SharedValueComponent;
Pass global state down the component tree without passing props.
Using the MorphStateProvider
and useMorphState
functions:
import React from 'react';
import { MorphStateProvider, useMorphState, valueOf } from 'morph-state';
const initialState = {
name: "Shridhar",
age: 10,
address: { stateCode: "TN", city: "Chennai" }
};
const callback = (value, { field, update, cancel }) => {
console.log(`State change at ${field} to ${value}`);
};
function App() {
return (
<MorphStateProvider initialState={initialState} config= onChange={callback}>
<RootComponent />
</MorphStateProvider>
);
}
function RootComponent() {
// As `interceptValues` is set to true, name property would be a proxy object even though it is a string
// You need to use helper methods like `valueOf` to use actual value of the property
const state = useMorphState();
return (
<div>
<p>Global State Name: {valueOf(state.name)}</p>
<ChildComponent />
</div>
);
}
function ChildComponent() {
// Though `interceptValues` is set to true at provider level, as it is set to `false` at hook, age property would be a raw numeric value instead of ruturning a proxy.
// You need not to use helper methods for such use case.
const age = useMorphState(state => state.age, { interceptValues: false });
return (
<div>
<p>Global State Age: {age}</p>
</div>
);
}
function NestedComponent() {
const stateCode = useMorphState(state => state.address.stateCode);
return (
<div>
<p>State Code: {valueOf(stateCode)}</p>
</div>
);
}
The morph-state
library utilizes config properties to determine whether to return raw data or a Proxy object:
Property | Type | Default | Description |
---|---|---|---|
interceptUndefined | boolean | false | Return proxy when a property is undefined |
interceptNull | boolean | false | Return proxy when a property is null |
interceptValues | boolean | false | Return proxy for non-object values (e.g., strings, booleans, etc.) |
interceptArrays | boolean | true | Return proxy for arrays |
interceptSpecialObjects | boolean | true | Return proxy for special objects like Map or Set |
interceptObjects | boolean | true | Return proxy for usual objects |
To simplify usage, helper methods can be called by passing the property retrieved from the state object. These methods only function correctly with Proxy objects:
Method | Description |
---|---|
withChangeHandler(state) |
Returns a callback method to set the value of the property being passed. This is equivalent to mutating that specific value directly. |
withEventHandler(state, beforeSet?) |
Returns a callback method to pass directly to DOM element’s onChange prop |
valueOf(state) |
Returns the raw value of the property being passed |
isNull(state) |
Checks if the property’s raw value is null |
isUndefined(state) |
Checks if the property’s raw value is undefined |
isNullOrUndefined(state) |
Checks if the property’s raw value is null or undefined |
isTruthy(state) |
Checks if the property’s raw value is truthy |
This hook manages mutable state within the component level.
Parameters:
initialState
(optional): The initial state object.configOrCallback
(optional): This can be a callback function triggered before a value update, a config object regulating Proxies, true
(all proxy behavior), or false
(no proxy behavior).const state = useMutableState({ count: 0 }, {
interceptUndefined: true,
interceptNull: true,
onChange: (value, { field, update, cancel }) => {
if (value < 0) cancel();
else if (value > 10) update(10);
}
});
Shared state across different applications within the same page.
Creates a store with the specified initial state and configuration.
Parameters:
initialState
(optional): The initial state object.configOrCallback
(optional): Similar options to useMutableState
.const store = createStore({ count: 0 }, {
interceptUndefined: true,
interceptNull: true,
onChange: (value, { field, update, cancel }) => {
if(value < 0) cancel();
}
});
Manage state within nested React components using Context API.
Provides global state using a React Context Provider.
Access stored state within components. Optional selector and config for specific properties or behaviors.
import { useMutableState, withChangeHandler, valueOf } from 'morph-state';
function App() {
const state = useMutableState({
user: {
name: 'John',
details: {
age: 30,
location: 'NY'
}
}
}, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<p>User Name: {valueOf(state.user.name)}</p>
{/* Start typing the text in following input field and the name would be updated automatically. */}
{/* `withEventHandler` does the trick for you */}
<input value= onChange={withEventHandler(state.user.name)} />
<button onClick={() => state.user.name = 'Doe'}>Change Name to Doe</button>
</div>
);
}
Replace the entire state with a new object:
import { useMutableState, valueOf, withChangeHandler } from 'morph-state';
function App() {
const state = useMutableState({ count: 0 }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<p>Count: {valueOf(state.count)}</p>
<button onClick={() => state.replace({ count: 100 })}>Set Count to 100</button>
</div>
);
}
import { useMutableState, valueOf, withChangeHandler } from 'morph-state';
function App() {
const state = useMutableState({ count: 0 }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<p>Count: {valueOf(state.count)}</p>
<button onClick={() => state.count++}>Increment</button>
<button onClick={() => state.reset()}>Reset</button>
</div>
);
}
import { useMutableState, valueOf, withEventHandler } from 'morph-state';
function Child({ onNameChange }) {
return (
<input type="text" placeholder="Enter name" onChange={onNameChange} />
);
}
function App() {
const state = useMutableState({ user: { name: '', details: { age: 0 }} }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<Child onNameChange={withEventHandler(state.user.name)} />
<p>User Name: {valueOf(state.user.name)}</p>
</div>
);
}
import { useMutableState, withEventHandler, valueOf } from 'morph-state';
function App() {
const state = useMutableState({ user: { name: 'John' } }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<input
type="text"
value={valueOf(state.user.name)}
onChange={withEventHandler(state.user.name)}
/>
<p>User Name: {valueOf(state.user.name)}</p>
</div>
);
}
import React, { useState, useEffect } from 'react';
import { useMutableState, withEventHandler, valueOf } from 'morph-state';
function RerenderCountDisplay({ onChange }) {
const [count, setCount] = useState(0);
// This will not be triggered multiple times as `withEventHandler` returns memoized function to avoid unnecessary rerenders
useEffect(() => {
setCount(c => c + 1);
}, [onChange]);
return <div>Re-render Count: {count}</div>;
}
function App() {
const state = useMutableState({
user: {
name: 'John'
},
}, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
return (
<div>
<RerenderCountDisplay onChange={withEventHandler(state.user.name)} />
<input value={valueOf(state.user.name)} onChange={withEventHandler(state.user.name)} />
<p>User Name: {valueOf(state.user.name)}</p>
</div>
);
}
Contrary to popular belief about immutability, morph-state
leverages controlled mutations to provide a convenient, intuitive API while ensuring performance optimization and maintaining React principles.
import { useMutableState, valueOf, withChangeHandler } from 'morph-state';
function DirectModification() {
const state = useMutableState({ counter: 0 }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: false
});
state.counter++; // You can use ++ only when `interceptValues` is set to false (which is default)
console.log(state.counter); // Outputs updated counter value
}
import { useMutableState } from 'morph-state';
const callback = (value, { field, update, cancel }) => {
if (value < 0) {
cancel();
}
};
const state = useMutableState({ count: 0 }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true,
onChange: callback
});
// or you can directly pass callback as second prop if you do not have any changes in default config
const state = useMutableState({ count: 0 }, callback);
When building applications that combine different technologies on the same page, having a mutable state management library like morph-state
can be incredibly powerful. This allows for seamless state sharing and interaction across various frameworks and libraries.
<div id="jquery-component">
<input type="text" id="shared-input" />
</div>
<div id="react-component"></div>
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
import { createStore, createHook, valueOf } from 'morph-state';
// Create store outside React component
const store = createStore({ sharedValue: '' }, {
interceptUndefined: true,
interceptNull: true,
interceptValues: true
});
$('#shared-input').on('change', function () {
store.state.sharedValue = $(this).val();
});
const useSharedValue = createHook(store, state => state.sharedValue);
function SharedComponent() {
const sharedValue = useSharedValue();
return (
<div>
<h1>Shared Value in React: {valueOf(sharedValue)}</h1>
</div>
);
}
ReactDOM.render(<SharedComponent />, document.getElementById('react-component'));
In this example, the morph-state
store allows both React and jQuery to interact with the same state seamlessly. The input field in the jQuery component updates the state, which then reflects in the React component.
Thank you for considering contributing to morph-state
! Please fork the repository and submit a pull request with a detailed description of your changes. By contributing, you agree that your contributions will be licensed under the same license as the project, MIT License.
For major changes, please open an issue first to discuss what you would like to change.
morph-state
provides a powerful and flexible state management solution for React applications. By leveraging mutable operations while ensuring compliance with React principles, it offers an optimized, intuitive state management approach.