import { createState } from "@persevie/statemanjs";
const counterState = createState(0);
counterState.subscribe((state) => {
if (Number.isInteger(state)) {
console.log("it's integer");
} else {
console.log("it's not integer");
}
});
function increaseCount() {
counterState.set(counterState.get() + 1);
}
type TransferElement = {
speed: number;
info: string;
error?: string;
};
const transferState = createState<TransferElement>({
speed: 0,
info: "start",
});
transferState.subscribe(
(state) => {
console.log("Oops, something seems to be broken");
},
{
notifyCondition: (state) => {
state.error !== undefined;
},
},
);
Table of Contents
Review
Statemanjs is a framework agnostic library for creating and managing the state of your JavaScript and NodeJS applications. Statemanjs is written in TypeScript so it has excellent support out of the box. statemanjs has integrations (delivered as npm packages) for front-end frameworks (react, vue, solid, etc.) that just make it easier to use. You can do without them if you wish.
Statemanjs has the highest performance and reliability, and adheres to a clear and strict API without a boilerplate. Statemanjs is highly scalable and suitable for both small and large projects.
Here are the basic principles of statemanjs:
- Performance
- Reliability
- Clear API
- Everything can be a state
Each of these principles will be discussed below.
Performance
Statemanjs was developed for JavaScript with all the features of this language in mind. Special attention is paid to single-threading and mutability.
Most state managers for JS take an immutable approach. This means that when the state changes, a copy of it is always created. It can be imagined like this - a new value of the object has come → the state is cloned → the state is updated → the updated state is returned. Now it does not look so scary, but let's add single-threading to this. Your application is forced to wait for the state update to complete before doing something else. It would be more logical to follow this approach - a new value of the object has come → the state is updated → the updated state is returned. The number of stages has been reduced, and therefore productivity has increased. “But you only threw out cloning, does it affect performance so much?” - Yes. In JavaScript, immutability is a very expensive operation. This means that the cloning step will take time, which can be spent, for example, updating the UI or performing another task. Add to this a huge amount of logic in your application, and the performance difference becomes more and more significant.
Statemanjs - takes a data mutability approach.
Reliability
The immutable approach ensures that your state is not accidentally changed, which is not the case with the mutable approach. For example, the state of Mobx can be changed anywhere and any way. You can bind the current state to a variable, and when the variable changes, the state will also change. Agree, it does not look very reliable. Statemanjs is arranged differently here as well. You can only change/create state through built-in methods. It is this API that guarantees the reliability of your state.
Clear API
As it was written above, any manipulations with your state are possible only through built-in methods, so they should be understandable and convenient.
The createState
method is used to create a state:
createState<T>(element: T): StatemanjsAPI<T>;
There are 6 methods for interacting with the state - set
, get
, subscribe
,
unsubscribeAll
,
getActiveSubscribersCount
,
update
.
Here is a detailed view of the API:
set(newState: T): boolean;
get(): T;
subscribe(subscriptionCb: SubscriptionCb<T>, subscriptionOptions?: SubscriptionOptions<T>): UnsubscribeCb;
unsubscribeAll(): void;
getActiveSubscribersCount(): number;
update(updateCb: UpdateCb<T>): void;
Everything can be a state
A state can be anything from primitives to complex and multidimensional objects. Just pass this to the createState
function and use the state with no extra effort.
const isLoading = createState(true);
const soComplexObject = createState({
1: { 2: { 3: { 4: { 5: [{ foo: "bar" }] } } } },
});
Usage
Installation and initialization the state
npm i @persevie/statemanjs
import { createState } from "@persevie/statemanjs";
const counterState = createState(0);
You can also pass in the type of your state if you are using TypeScript:
import { createState } from "@persevie/statemanjs";
type User = {
name: string;
age: number;
};
const userState = createState<User>({ name: "Finn", age: 13 });
To get the current state, use the get
method.
const counterState = createState(1);
const counter = counterState.get();
Subscribe to changes
The subscribe
method takes a callback function and executes it on every state change. This callback function accepts the updated state.
const counterState = createState(0);
counterState.subscribe((state) => {
if (Number.isInteger(state)) {
console.log("it's integer");
} else {
console.log("it's not integer");
}
});
You can set a condition, notifyCondition
, under which the callback will be called. This condition is the second and optional parameter. If there is no condition, then the callback will fire on every state change. notifyCondition
also accepts the updated state.
const counterState = createState(0);
counterState.subscribe(
(state) => {
console.log("it's integer");
},
{ notifyCondition: (state) => Number.isInteger(state) },
);
The subscribe
method returns a callback to unsubscribe.
const counterState = createState(0);
const unsub = counterState.subscribe(
(state) => {
console.log("it's integer");
},
{ notifyCondition: (state) => Number.isInteger(state) },
);
unsub();
To unsubscribe all active subscriptions from a state, use the unsubscribeAll
method;
counterState.unsubscribeAll();
Sometimes you need to find out how many active subscriptions a state has, for this there is a getActiveSubscribersCount
method.
const subscribersCount = counterState.getActiveSubscribersCount();
State change
There are two ways to change the state - set
and update
. The set
method completely changes the state and is great for primitives and simple states.
const counterState = createState(0);
counterState.subscribe(
(state) => {
console.log("it's integer");
},
{ notifyCondition: (state) => Number.isInteger(state) },
);
counterState.set(2);
counterState.set(counterState.get() * 2);
The update
method is suitable for complex states (objects and arrays) in which only part of the state needs to be changed. The update
method accepts the current state.
import { createState } from "@persevie/statemanjs";
type User = {
name: string;
age: number;
isOnline: boolean;
hobbyes: Array<string>;
};
const userState = createState<User>({
name: "Finn",
age: 13,
isOnline: false,
hobbyes: [],
});
userState.update((state) => {
state.isOnline = !state.isOnline;
});
userState.update((state) => {
state.hobbyes.push("adventure");
});
Benchmark
The benchmark was run on a MacBook Pro, m1, 16gb.
You can run it on your device.
Below is a comparison table with other popular state managers.
This table is the result of the benchmark, which adds elements to the array (state), in the amount of 100, 500, 1000, 5000, 10000, 20000, 40000, 80000, 160000, 320000, 640000, 1280000, 2560000.
Each case was run 10 times (for all state managers) and the average value was recorded in a table.
*❌ - means error during benchmark.
**Results in seconds
Name | 100 | 500 | 1000 | 5000 | 10000 | 20000 | 40000 | 80000 | 160000 | 320000 | 640000 | 1280000 | 2560000 |
---|
statemanjs | 0.1256750002503395 | 0.41603749953210356 | 0.6447918005287647 | 1.5476418003439902 | 2.6794791001826526 | 5.217970700562001 | 9.459733299911022 | 16.601908500120043 | 33.24702489995398 | 59.56564570013434 | 125.51550420001149 | 270.7810873998329 | 532.6712166998535 |
redux | 0.12983310036361217 | 1.3561332002282143 | 4.40060419999063 | 266.9328539993614 | 1009.34137509875 | 4177.273962600157 | 19662.740120899678 | 105281.26535429992 | ❌ | ❌ | ❌ | ❌ | ❌ |
mobx | 0.7277332998812198 | 2.706258299946785 | 4.108445800095796 | 13.18133749999106 | 24.971716599538922 | 47.74443760029972 | 99.50159570015967 | 200.66793330013752 | 452.0446125999093 | 1100.9856083998457 | ❌ | ❌ | ❌ |
xstate | 1.5114331997931003 | 5.076700000837445 | 9.003429099917412 | 26.321991700306533 | 47.318245799839495 | 92.908674999699 | 184.9398877006024 | 353.5915873996913 | 739.1568584999535 | 1552.7339956998826 | 3419.594879200682 | 8327.197879200055 | 22651.708679099753 |
effector | 0.45675409957766533 | 1.2237749002873897 | 1.6501832995563745 | 3.2516458999365567 | 4.759345900639891 | 8.276599999889731 | 13.910462499782444 | 23.23244180008769 | 46.63204999999143 | 81.9146542005241 | 174.61498729977757 | 378.94245009999725 | 745.4281083000824 |
As you can see from the table, statemanjs shows the best results. Pay close attention to the performance difference between statemanjs and mobx. Both state managers rely on state mutability, but the difference in the implementation of this approach sets them apart. In other words, variability is their only similarity.
When looking at this table, keep in mind that the performance of most state managers will be slower on real projects. For example, Redux currently has only one body, in reality there will be more reducers. On the other hand, Statemanjs scales well in both depth (state size) and width (number of states).
Don't forget that the benchmark was run in the NodeJS environment, everything will be slower in the browser. Also, user devices can be different, which also affects performance.
Integrations
Statemanjs is framework agnostic and can be used without additional packages. But for convenience, there are packages for the most popular frameworks - react, vue, solid. Statemanjs supports svelte out of the box and doesn't need any additional packages.
To work with additional packages, the main statemanjs package is required.
For contributors
See CONTRIBUTING.md.