npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

morph-state

v0.0.1

Published

A fine-grained mutable state management library for React

Downloads

5

Readme

License: MIT

morph-state Documentation

Table of Contents

  1. Introduction
  2. Key Features
  3. Installation
  4. Usage
    1. Component-wise State Management
    2. Global State Outside Components
    3. Context Integration for Global State
  5. API Reference
    1. Interceptor Config Properties
    2. Helper Methods
  6. Advanced Usage
    1. Working with Nested Properties
    2. Replacing the Entire State
    3. Resetting the State
    4. Passing State to Child Components
    5. Using State as Change Handler
    6. Minimizing Re-renders
  7. React Standards Compliance and Justification for Mutations
    1. Why Mutations?
    2. Ensuring React Principles
  8. More Usage Examples
  9. Contributing
  10. Conclusion

Introduction

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.

Key Features

  1. Component-wise State Management:

    • Like React's useState but allows deeper property mutations.
    • Prevents unnecessary re-renders.
    • Includes callback mechanisms for controlled state changes.
  2. Global State Outside Components:

    • Manage state outside React components.
    • Share state across different applications on the same page (e.g., JQuery and React).
  3. Context-based Global State:

    • Manage global state using React Context API.
    • Provides <MorphStateProvider> and useMorphState hooks.

Installation

You can install morph-state using your preferred package manager:

Using NPM

npm install morph-state

Using Yarn

yarn add morph-state

Using PNPM

pnpm add morph-state

Usage

Component-wise State Management

Basic Usage

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>
    );
}

Global State Outside Components

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

Using Hooks Inside Components:

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>
    );
}

Create Slice Hook Example:

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>
    );
}

State Sync from Outside:

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;

Context Integration for Global State

Pass global state down the component tree without passing props.

Define Provider and Context Hooks:

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={{ interceptUndefined: true, interceptNull: true, interceptValues: true }} 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>
    );
}

Accessing Nested Properties:

function NestedComponent() {
    const stateCode = useMorphState(state => state.address.stateCode);
    return (
        <div>
            <p>State Code: {valueOf(stateCode)}</p>
        </div>
    );
}

API Reference

Interceptor Config Properties

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 |

Helper Methods

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 |

Component-wise State Management

useMutableState

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);
    }
});

Global State

Shared state across different applications within the same page.

createStore

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();
    }
});

Context-based Global State

Manage state within nested React components using Context API.

<MorphStateProvider initialState={state} config={config} onChange={callback} />

Provides global state using a React Context Provider.

useMorphState(selector?, config?)

Access stored state within components. Optional selector and config for specific properties or behaviors.

Advanced Usage

Using event handlers

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={{valueOf(state.user.name)}} onChange={withEventHandler(state.user.name)} />
            <button onClick={() => state.user.name = 'Doe'}>Change Name to Doe</button>
        </div>
    );
}

Replacing the Entire State

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>
    );
}

Resetting the State

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>
    );
}

Passing State to Child Components

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>
    );
}

Using State as Change Handler

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>
    );
}

Minimizing Re-renders

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>
    );
}

React Standards Compliance and Justification for Mutations

Why Mutations?

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.

Ensuring React Principles

  1. Controlled Mutations: Ensure React re-renders components only when necessary.
  2. Efficient Updates: Proxy mechanism to communicate state changes efficiently.
  3. Component Isolation: Subscriptions only to necessary state properties.
  4. Declarative State Management: Ensure the interface remains declarative.
  5. Memoization: Prevent unnecessary re-renders.

More Usage Examples

Directly Mutate Properties

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
}

Callback Usage

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);

Powerful Use Cases

Interaction Between Different Technologies

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.

Example: jQuery and React Interaction

<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.

Contributing

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.

Conclusion

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.