deep-state-manager v4.1.15
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)
- CODESANDBOX: https://codesandbox.io/s/deep-statemanager-5niqz
- CREATE-REACT-APP: scroll to the bottom of this readme for full copy-paste instructions
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' }}> <TodoComponent substate='state{props.substate ? `.${props.substate}` : ''}' /> <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>{" "} <ExcelComponent substate='state {props.substate ? `.${props.substate}` : ""}' /> {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
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago