deep-state-observer for high performance apps
Deep state observer is an state management library which will trigger an update only when specified object node was changed.
You don't need to reevaluate or re-render whole app/component when only one portion of the state was modified.
Used in the right hands can significantly increase performance!
Deep state observer is framework agnostic with node and browser support, so you can use it in most of your projects.
Install
npm i deep-state-observer
Examples
here and here
Usage
Svelte example
import { onDestroy } from 'svelte';
import State from 'deep-state-observer';
const state = new State({
some: 'value',
someOther: {
nested: 'value'
}
},
{ delimiter:'.' , notRecursive:';', param: ':', log: console.log }
);
let subscribers = [];
let nestedValue;
subscribers.push(
state.subscribe('someOther.nested', (value, eventInfo) => {
nestedValue = value;
})
);
let some;
subscribers.push(
state.subscribeAll(['some', 'someOther'], (value, eventInfo) => {
if (eventInfo.path.resolved === 'some') {
some = value;
} else if (eventInfo.path.resolved === 'someOther') {
nestedValue = value.nested;
}
})
);
state.update('someOther.nested', (currentValue) => {
return 'new value';
});
subscribers.push(
state.subscribe('some', (value, eventInfo) => {
state.update('someOther.nested', (oldValue) => {
return 'nested changed too';
});
})
);
subscribers.push(
state.subscribe('some', (value, eventInfo) => {
state.update('someOther.nested', 'nested changed too');
);
onDestroy(() => {
subscribers.forEach((unsubscribe) => unsubscribe());
});
Update and proxy
You can use proxy instead of state.update
function.
Proxy is better because values are linted;
const state = new State({ x: { y: { z: 10 } } });
state.update("x.y.z", 20);
state.proxy.x.y.z = 20;
state.$$$.x.y.z = 20;
state.update("x.y.z", (value) => {
return value + 10;
});
state.proxy.x.y.z = (value) => {
return value + 10;
};
state.$$$.x.y.z = (value) => {
return value + 10;
};
All objects and arrays are reactive (proxies)
const state = new State({ x: { y: { z: 10 } } });
let counter = 0;
state.subscribe("x.y.z", () => {
counter++;
});
const z = state.get("x.y.z");
z = 20;
const y = state.get("x.y");
y.z = 20;
state.proxy.x = { y: { z: 30 } };
Wildcards
import { onDestroy } from "svelte";
import State from "deep-state-observer";
const state = new State({
some: { thing: { test: 0 } },
someOther: { nested: { node: "ok" } },
});
let subscribers = [];
subscribers.push(
state.subscribe("someOther.*.n*e", (value, eventInfo) => {
})
);
state.update("some.*.test", "test");
onDestroy(() => {
subscribers.forEach((unsubscribe) => unsubscribe());
});
Named wildcards (parameters)
import { onDestroy } from "svelte";
import State from "deep-state-observer";
const state = new State({
items: [{ val: 1 }, { val: 2 }, { val: 3 }],
byId: {
1: { val: 1 },
2: { val: 2 },
3: { val: 3 },
},
});
let subscribers = [];
subscribers.push(
state.subscribe("items.:index.val", (value, eventInfo) => {
})
);
subscribers.push(
state.subscribe("byId.:id.val", (value, eventInfo) => {
})
);
onDestroy(() => {
subscribers.forEach((unsubscribe) => unsubscribe());
});
Wildcard bulk operations (better performance)
import { onDestroy } from "svelte";
import State from "deep-state-observer";
const state = new State({
byId: {
1: { val: 1 },
2: { val: 2 },
3: { val: 3 },
},
});
let subscribers = [];
subscribers.push(
state.subscribe(
"byId.:id.val",
(bulk, eventInfo) => {
},
{ bulk: true }
)
);
subscribers.push(
state.subscribe(
"byId.*.val",
(bulk, eventInfo) => {
},
{ bulk: true }
)
);
onDestroy(() => {
subscribers.forEach((unsubscribe) => unsubscribe());
});
Observe only chosen node changes (not recursive, not nested, for immutable data)
import { onDestroy } from "svelte";
import State from "deep-state-observer";
const state = new State({
some: "value",
someOther: { nested: { node: "ok" } },
});
let subscribers = [];
subscribers.push(
state.subscribe("someOther;", (value, eventInfo) => {
})
);
subscribers.push(
state.subscribe("someOther.nested;", (value, eventInfo) => {
})
);
state.update("someOther.nested.node", "modified");
onDestroy(() => {
subscribers.forEach((unsubscribe) => unsubscribe());
});
Update and notify only specified listeners
const state = new State({
one: { two: { three: { four: 4 } } },
});
state.subscribe("one.two", (val, eventInfo) => {
});
state.subscribe("one.two.three.four", (val, eventInfo) => {
});
state.update("one.two", { three: { four: 44 } }, { only: ["*.four"] });
mute / unmute changes
It will work with wildcards as update values, as muted paths or both.
const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" });
const values = [];
state.subscribe("x.i.o", (val) => {
values.push(val);
});
state.mute("x.*.o");
state.update("x.i.o", "oo");
state.unmute("x.*.o");
state.update("x.i.o", "ooo");
state.mute("x");
state.update("x.i.o", "oooo");
state.unmute("x");
state.mute("x;");
state.update("x.i.o", "oooo");
You can also mute specific listeners (functions)
const state = new State({ x: { z: "z", i: { o: "o" } }, y: "y" });
const values = [];
function listener1() {
values.push("1");
}
function listener2() {
values.push("2");
}
state.subscribe("x.i.o", listener1);
state.subscribe("x.i.o", listener2);
state.mute(listener2);
values.length = 0;
state.update("x.i.o", "oo");
state.unmute(listener2);
values.length = 0;
state.update("x.i.o", "oo");
ignore
You can watch for object changes but also at the same time ignore specified nodes.
Ignore option will work with wildcards.
const state = new State({ one: { two: { three: { four: { five: 0 } } } } });
const values = [];
state.subscribe(
"one.two.three",
(val) => {
values.push(val);
},
{ ignore: ["one.two.three.four"] }
);
state.update("one.two.three.four.five", 1);
state.update("one.two.three.*.five", 1);
state.update("one.two.three.four", 1);
state.update("one.two.*.four", 2);
state.update("one.two.three", 1);
multi
You can collect updates and execute them at once later - useful when used with groups.
const state = new State({
x: { y: { z: { a: { b: "b" } } } },
c: { d: { e: "e" } },
});
const values = [];
state.subscribe("x.y.z.a.b", (val, eventInfo) => {
values.push(val);
});
state.subscribeAll(
["x.y.*.a.b", "c.d.e"],
(val, eventInfo) => {
values.push("all");
},
{ group: true }
);
const multi = state.multi(true);
multi.update("x.y.z.a.b", "bb");
multi.update("c.d.e", "ee");
multi.done();
group
With subscribeAll you can group listeners to fire only once if one of the path is changed.
Grouped listeners always are bulk listeners.
const state = new State({
"n-1": {
"n-1-1": {
id: "1-1",
val: "v1-1",
},
"n-1-2": {
id: "1-2",
val: "v1-2",
},
},
});
const results = [];
function fn(bulk, eventInfo) {
results.push(eventInfo.path);
}
state.subscribeAll(["n-1.n-1-1.id", "n-1.n-1-2.val"], fn, {
group: true,
});
state.subscribeAll(["n-1"], fn);
state.update("n-1.*.id", "new id");
state.update("n-1.*.id", "new id 2");
collect
You can start collecting changes and execute it as multi later - performance optimization for groups.
It is just global multi.
const state = new State({
x: { y: { z: { a: { b: "b" } } } },
c: { d: { e: "e" } },
});
const values = [];
state.subscribe("x.y.z.a.b", (val, eventInfo) => {
values.push(val);
});
state.subscribeAll(
["x.y.*.a.b", "c.d.e"],
(val, eventInfo) => {
values.push("all");
},
{ group: true }
);
state.collect();
state.update("x.y.z.a.b", "bb");
state.update("c.d.e", "ee");
state.executeCollected();
Trace
You can easily track traces
state.startTrace(name: string, additionalData: any = null): string
start tracing
state.stopTrace(id:string): Trace
get current trace
state.saveTrace(id:string):Trace
save trace on the stack
state.getSavedTraces(): Trace[]
get all traces from the stack
const state = new State({
p1: "p1v",
p2: "p2v",
x1: "x1v",
x2: "x2v",
});
state.subscribe("p1", (val, eventInfo) => {
const trackId = state.startTrace("p1", eventInfo);
state.update("p2", "p2v-");
state.saveTrace(trackId);
});
state.subscribe("p2", (val, eventInfo) => {
const trackId = state.startTrace("p2", eventInfo);
state.update("x2", "x2v-");
state.saveTrace(trackId);
});
const result = state.getSavedTraces();
console.log(result);
Debug
state.subscribe("something", () => {}, {
debug: true,
source: "your.component.name.or.something",
});
state.update("something", "someValue", {
debug: true,
source: "your.component.name.or.something",
});
Vue example
import State from 'deep-state-observer';
const state = new State({ test: 1 });
const subscribers = [];
export default {
provide: { state },
};
export default {
template: `<div>test is equal to: {{test}}</div>`,
inject: ['state'],
data() {
return {
test: 0,
};
},
created() {
subscribers.push(
this.state.subscribe('test', (test) => {
this.test = test;
})
);
},
beforeDestroy() {
subscribers.forEach((unsubscribe) => unsubscribe());
},
};