4.1.15 • Published 5 years ago

deep-state-manager v4.1.15

Weekly downloads
4
License
WTFPL
Repository
github
Last release
5 years ago

deep-state-manager

Slimmer than Immer :). As fast as Immer!

Managing state has a lot of shapes. Redux is the most easy to grasp and have state management under control when application starts to get really big. But you need to make custom reducer and invent new name for each action which takes time and complicates workflow. In more than 90% of cases, custom reducer is not needed at all, because we perform really simple transformations.

Deep-state-manager uses simple object like traversing, but with more control what we want to select in state. When we select or better said isolate the part of the state that we want to change, we use trigger words like set, update, delete, prepend, append transformations and provide payload if needed and that is it.

If you need to, you can easily integrate custom reducer on top of it and have access to full Redux experience where you need to.

Try it instantly (React implementation)

Features

  • extreme performance that is on par with very fast immer state management library
  • usable on both frontend and backend apps (import, require)
  • intuitive path definition with safe function to prevent path corruption
  • unlimited traversing thru objects and array of objects to hit deepest part of state
  • versioning of each level of the state, to signify exactly what changed deeply
  • heavily tested on complex examples and deep state changes
  • ability to easily insert ID or string into path definition in loops
  • extremely fast rendering of react components without complex memoization functions
    import React, { useMemo } from 'react'
    function SomeComponent() {
      return useMemo(()=> {
        return (
          <>
            <div>Will render only on version number change!</div>
          </>
        )
      },[state.some.substate.___version, state.other.substate.___version])
    }
  • easy inclusion in all react components via down provided hooks implementation

    //
    // 1. use provided state.js file for implementation at the bottom of this readme
    //
    
    //  
    // 2. add these lines to root component
    //
    import { useGlobal, StateContext } from "./state";
    const { state, dispatch } = useGlobal();
    function App() {
      return (
        <>
          <StateContext.Provider value={{ state, dispatch }}>
            ...
          </StateContext.Provider>
        </>
      )
    }
    
    //
    // 3. add to any child component
    //
    import { useLocal } from "./state";
    const { state, stateRoot, dispatch, dispatchRoot, safe } = useLocal();
    // state - local state
    // stateRoot - entire state
    // dispatch - send actions to local state only
    // dispatchRoot - send actions to entire state
    // safe escapes path, ie. const x="dont.break.state"; d(safe`some.substate.${x}.substate`);

Minimal example

In this case we need to update all planets to have tracking key present which do not have name deimos. Here is the one-liner implementation you need to write to achieve this (versioning keys are purposefully left out in this minimal example):

START STATE

let state = {
  planets: [
    {  
      name: 'earth', 
      satellites: [
        { name: 'moon' },
      ],
    },
    {
      name: 'mars', 
      satellites: [
        { name: 'phobos' },
        { name: 'deimos' },
      ],
    },
  ],
}

EXECUTE STATE UPDATE

d(state, `planets..satellites.name!="deimos"#update`, { track: true })

NEW STATE

{
  planets: [
    {  
      name: 'earth', 
      satellites: [
        { name: 'moon', track: true }, // UPDATED
      ],
    },
    {
      name: 'mars', 
      satellites: [
        { name: 'phobos', track: true }, // UPDATED
        { name: 'deimos' },
      ],
    },
  ],
}

Usage

Simple syntax

state = d(state, action, payload?)

Path traversing

The "." (dot) is the separator for the path, it slices the path into unique parts.

Each unique part can be:

  • a key (can be an array or object or just value)
    • list
  • match single value in array with any type of primitive
    • =2 > number 2 in array (1,2,3)
    • =true > boolean true in array (true, false)
    • ="manager" > string in array
    • numbers.>=3 > all array keys with number >=3
    • !=2 > gets 1,3 from array (1,2,3)
  • match multiple (separator: |) values in array with any type of primitive
    • =2|=3 > numbers 2 and 3 in array (1,2,3)
    • !=2|!=3 > gets 1 from array (1,2,3)
    • =true|=false > boolean true and false in array (true, false)
    • ="manager"|="people" > multiple strings in array
    • numbers.>=2|<=4 > all array keys with number between 2 and 4
  • match multiple (separator: | and &) keys in objects objects in array by key value
    • id=1|name="manager" > match any of the keys
    • id=1&name="manager" > match all keys
    • id!=1|name="manager" > all objects in array that do not have id:1 and have name:manager
    • id=1&name!="manager" > all objects in array that do have id:1 and do not have name:manager
    • salary>=15|age=20 > match any with one greater and one equal to
    • salary>=15&age=20 > match all with one greater and one equal to
    • salary>=15|age<=25 > match all with one greater and one less than
    • salary>=15&age<=25 > match all with one greater and one less than
  • match all elements in array
    • .. > or empty value between dots means all elements in array
// basic set/update/get per key
user.list#set // will overwrite the key
user.list#update // will spread the keys from payload
user.list#del // will delete the key

// array transform basic on user.list = []
user.list#prepend 
user.list#append
user.list#...append
user.list#...appendreverse
user.list#...prepend
user.list#...prependreverse

// obj # array ALL elements # obj
user.list..friends..#update
user.list..friends.name="manager"#update

// array of values filtered by inclusion
user.list.=2|=3#del
user.list.=true#del
user.list.="manager"#del
user.list.={"name":"manager"}#del

// array of values filtered by exclusion
user.list.!=2|!=3#del
user.list.!=true#del
user.list.!="manager"#del
user.list.!={"name":"manager"}#del

// obj # array of obj filtered
user.list.id=1|name="manager"#set
user.list.id=3&name="manager"#set
user.list.id=1|name="manager"#update
user.list.id=2&name="manager"#update
user.list.id=1|name="manager"#del
user.list.id=3&name="manager"#del

// obj # array of obj filtered # obj # array of values
user.list.name="manager".list.=2|=3#del

// obj # array of obj filtered # array
user.list.id=1|name="manager".friends#append 
user.list.id=1|name="manager".friends#prepend
(trigger can be: append, appendreverse, ...append, ...appendreverse)
(trigger can be: prepend, prependreverse, ...prepend, ...prependreverse)

// obj # array of obj filtered # obj # arr/obj
user.list.id=1|name="manager".friends.="person"#del
user.list.id=1|name="manager".friends.name="person"#del
user.list.id=1|name="manager".friends.name="person"|name="me"#del
user.list.id=1.friends.name="manager"#del
(trigger can be: del, update, set)

// nagative matching for array keys and objects in array
user.list.!=2#del (or update or set)
user.list.id!=1|name="manager"#del
user.list.id=1&name!="manager"#del
(trigger can be: del, update, set)

// go thru object with specific keys, then find object with specific keys in array
company.roles.age=20&salary>=15.friends.name="two"

Copy-paste starter (use in node or runkit to test)

const { d, safe } = require("deep-state-manager")

let state = {
  company: {
    ___version: 10,
    employees: 140,
    roles: [
      { role: 'manager' }, 
      { role: 'ceo' },
    ],
  },
  planets: {
    list: ['saturn', 'neptune']
  }
}

state = d(state, `planets.list#append`, 'mars')
console.log(state)
// OUTPUTS:
// {
//   ___version: 1,
//   company: {
//     ___version: 10,
//     employees: 140,
//     roles: [
//       { role: 'manager' }, 
//       { role: 'ceo' },
//     ],
//   },
//   planets: {
//     ___version: 1
//     list: ['saturn', 'neptune', 'mars']
//   }
// }

const roleName = 'ceo'
state = d(state, safe`company.roles.role="${roleName}"#update`, { email: 'ceo@somefakeceosite.com' })
console.log(state)
// OUTPUTS:
// {
//   ___version: 2,
//   company: {
//     ___version: 11,
//     employees: 140,
//     roles: [
//       { role: 'manager' }, 
//       { role: 'ceo', email: 'ceo@somefakeceosite.com', ___version: 1 },
//     ],
//   },
//   planets: {
//     ___version: 1
//     list: ['saturn', 'neptune', 'mars']
//   }
// }

Performance

deep-state-manager (on runkit executed in 1.5 miliseconds)

baseState = d(baseState, safe`todos#append`, {todo: 'tweet about it'})
baseState = d(baseState, safe`data.id=1#del`)
baseState = d(baseState, safe`data.id=2.age#set`, 100)

immer version of the same actions above (on runkit executed in 1.3 miliseconds)

baseState = immer(baseState, draftState => {
    draftState.todos.push({todo: "Tweet about it"})
})
baseState = immer(baseState, draftState => {    
    draftState.data = draftState.data.filter(dat=>{
       if (dat._id === 1) { return false }
       return true
    })
})
baseState = immer(baseState, draftState => { 
    draftState.data = draftState.data.map(dat=>{
       if (dat._id === 2) { return {...dat, age: 100} }
       return dat
    })
})

!!! warning: immer will break if you do not set ie. key 'todos' in advance, but deep-state-manager will create that path for you automagically.

!!! information: in github repo you have copy-paste examples that you can run on runkit and compare immer and deep-state-manager performance yourself

Advanced usage

If you need more control, you can prepare the payload before sending it to d() function. Function d() can hit anything in the state with simple path.

Without preparing the payload in advance you can do a lot of transformations automatically with proper path. Most used cases are covered and tested.

React integration in Create React App starter

Info: A lot of code is there to beautify the final result, so it is more visible. You will be able to track changes in state realtime to get good feeling how exactly deep-state-manager works.

  • make sure you have node.js installed properly

  • create react app with by running in terminal:

    npx create-react-app app1
    cd app1
    npm install --save deep-state-manager get-value
  • open VSCODE, ATOM, WEBSTORM, SUBLIME, ... in the root of /app1

  • src/state.js

    import { createContext, useContext,  useReducer } from 'react';
    import { d, safe } from 'deep-state-manager';
    import getValue from 'get-value'
    
    export const initial = {
        ___version: 0
    };
    
    export const StateContext = createContext(initial);
    
    export function useGlobal() {
      const [state, dispatch] = useReducer((globalState, globalAction) => {
        const [path, payload] = globalAction;
        return d(globalState, path, payload);
      }, initial);
    
      return { state, dispatch };
    }
    
    export function useLocal(substate) {
      const { state: stateRoot, dispatch: dispatchRoot } = useContext(StateContext);
    
      const dispatch = action => value => {
        const path = `${(substate && `${substate}.`) || ''}${action}`;
        const payload = value && value.target ? value.target.value || '' : value; // event or value
        dispatchRoot([path, payload]);
      };
    
      const state = getValue(stateRoot, substate)
    
      return { state: state || { ___version: 0 }, stateRoot, dispatch, dispatchRoot, safe };
    }
  • src/App.js

    import React from 'react';
    import { useGlobal, StateContext } from './state';
    
    import TodoComponent from "./todo-component";
    import ExcelComponent from "./excel-component";
    
    function App() {
      const { state, dispatch } = useGlobal(); // create state
    
      return (
        <>
          <StateContext.Provider value={{ state, dispatch }}>
            <div style={{ padding: 20 }}>
              <h1 style={{ textAlign: 'center', paddingBottom: 20 }}>Deep state manager testgrounds</h1>
              <TodoComponent substate='public.user' />
              <TodoComponent substate='company.somename.management' />
              <ExcelComponent substate="public.calculations" columns="2" rows="3" />
              <ExcelComponent substate="company.wide.calculations" />
              <pre style={{ backgroundColor: '#333', color: 'white', padding: 10}}>
                STATE:
                {JSON.stringify(state, null, 2)}
              </pre>
            </div>
          </StateContext.Provider>
        </>
      );
    }
    
    export default App;
  • src/todo-component.js

    import React, { useMemo } from 'react';
    import { useLocal } from './state';
    
    function TodoComponent(props) {
      const { state, dispatch, safe } = useLocal(props.substate); // substate isolation in motion
    
      return useMemo(() => {
        console.log({ RENDERING_COMPONENT: `<TodoComponent substate='state${props.substate ? `.${props.substate}` : ''}' />` })
        return (
          <>
            <div style={{ marginTop: 20, padding: 10,  backgroundColor: '#eee' }}>
            &lt;TodoComponent substate='state{props.substate ? `.${props.substate}` : ''}' /&gt;
              <br /><br />
              <input
                value={state.new || ''}
                onChange={e => {
                  dispatch(`new#set`)(e.target.value);
                }}
              />
              <button
                type="button"
                onClick={() => {
                  dispatch(`list#prepend`)({ name: state.new, key: +new Date() });
                  dispatch(`new#del`)();
                }}
              >
                Add new todo item
              </button>
              {state &&
                state.list &&
                state.list.map(item => (
                  <div key={item.key}>
                    <button type="button" onClick={dispatch(safe`list.key=${item.key}#del`)}>
                      Delete
                    </button>
                    {' '}{item.name}
                  </div>
                ))}
            </div>
          </>
        )
      }, [state.___version])
    }
    
    export default TodoComponent;
  • src/excel-component.js

    import React, { useMemo } from "react";
    import { useLocal } from "./state";
    
    function ExcelComponent(props) {
      const { state, dispatch, safe } = useLocal(props.substate); // substate isolation in motion
    
      return useMemo(() => {
        console.log({
          RENDERING_COMPONENT: `<ExcelComponent substate='state${
            props.substate ? `.${props.substate}` : ""
          }' />`
        });
        return (
          <>
            <div style={{ marginTop: 20, padding: 10, backgroundColor: "#eee" }}>
              <button
                onClick={() => {
                  dispatch(safe`tables#set`)({
                    data: Array.from(Array(Number(props.rows || 5)).keys()).map(
                      rowNumber => {
                        return Array.from(
                          Array(Number(props.columns || 5)).keys()
                        ).reduce((av, columnNumber) => {
                          return {
                            ...av,
                            [`R${rowNumber}C${columnNumber}`]: ""
                          };
                        }, {});
                      }
                    )
                  });
                }}
              >
                Create table ({props.columns || 5}x{props.rows || 5})
              </button>{" "}
              &lt;ExcelComponent substate='state
              {props.substate ? `.${props.substate}` : ""}' /&gt;
              {state &&
                state.tables &&
                state.tables.data &&
                state.tables.data.map(row => (
                  <div>
                    {Object.keys(row)
                      .filter(key => !["id", "___version"].includes(key))
                      .map(key => (
                        <input
                          type="text"
                          value={row[key] || ""}
                          onChange={e => {
                            dispatch(`tables.data.${key}="${row[key]}".${key}#set`)(
                              e.target.value
                            );
                            console.log(
                              `tables.data.${key}="${row[key]}".${key}#set`
                            );
                          }}
                        />
                      ))}
                  </div>
                ))}
            </div>
          </>
        );
      }, [state.___version]);
    }
    
    export default ExcelComponent;
  • run the app and look how app works and state changes in realtime

    npm start
4.1.15

5 years ago

4.1.13

5 years ago

4.1.11

5 years ago

4.1.9

5 years ago

4.1.7

5 years ago

4.1.3

5 years ago

4.1.1

5 years ago

4.0.15

5 years ago

4.0.11

5 years ago

4.0.9

5 years ago

4.0.7

5 years ago

4.0.3

5 years ago

4.0.2

5 years ago

4.0.1

5 years ago

4.0.0

5 years ago

3.0.0

5 years ago

2.2.2

5 years ago

2.2.1

5 years ago

2.2.0

5 years ago

2.1.1

5 years ago

2.1.0

5 years ago

2.0.3

5 years ago

2.0.2

5 years ago

2.0.1

5 years ago

2.0.0

5 years ago

1.4.9

5 years ago

1.4.8

5 years ago

1.4.7

5 years ago

1.4.5

5 years ago

1.4.4

5 years ago

1.4.3

5 years ago

1.4.2

5 years ago

1.4.1

5 years ago

1.4.0

5 years ago

1.3.2

5 years ago

1.3.1

5 years ago

1.3.0

5 years ago

1.2.0

5 years ago

1.1.3

5 years ago

1.1.2

5 years ago

1.1.1

5 years ago

1.1.0

5 years ago

1.0.3

5 years ago

1.0.2

5 years ago

1.0.1

5 years ago

1.0.0

5 years ago