manate is a lightweight, intuitive state management library that keeps things simple. Pronounced like "many-it" and short for "manage state". manate lets you handle state with ease across both frontend and backend.
- Effortless to use: No complex syntax – your state is just a JavaScript object.
- Zero dependencies: Clean and minimal, without any baggage.
- Universal: Works seamlessly on both frontend and backend environments.
- Lightweight: Around 500 lines of code. Simplicity without sacrificing power.
- TypeScript-ready: First-class TypeScript support for robust, type-safe development.
Start using manate and manage your state effortlessly!
yarn add manate
import { manage } from 'manate';
class Store {
count = 0;
increase() {
this.count += 1;
}
}
const store = manage(new Store());
You don't need to declare a class if you don't want to.
You don't need to create a function if you don't want to.
import { manage } from 'manate';
const store = manage({ count: 0 });
store.count += 1; // change data directly without a function
import { auto } from 'manate/react';
const App = auto((props: { store: Store }) => {
const { store } = props;
return (
<Space>
<Button
onClick={() => {
store.count -= 1;
}}
>
-
</Button>
{store.count}
<Button onClick={() => store.increase()}>+</Button>
</Space>
);
});
In the sample above I showed you two ways to update data:
- update it directly:
store.count -= 1
- update it through a member function:
store.increase()
So basically there is no restrictions. Just read/update as how you read/update a plain object.
You may use it without React.
import { $, manage, type ManateEvent } from 'manate';
class Store {}
const store = manage(new Store());
$(store)
is an EventEmitter which will emit events about read/write to store. You can subscribe to events:
$(store).on((event: ManateEvent) => {
// do something with event
});
Please note that, this EventEmitter
is not the same as EventEmitter
in Node.js. It's a custom implementation.
Sometimes we only want to keep a reference to an object, but we don't want to track its changes.
You may exclude
it from being tracked.
import { exclude, manage } from 'manate';
class B {
public c = 1;
}
class A {
public b = exclude(new B());
}
const a = new A();
const ma = manage(a);
ma.b.c = 4; // will not trigger a set event because `ma.b` is excluded.
You may invoke the exclude
method at any time.
You may invoke the exlcude method before or after you manage the object:
For more details, please refer to the test cases in ./test/exclude.spec.ts.
The signature of run
is
function run<T>(
managed: T,
func: Function,
): [result: any, isTrigger: (event: ManateEvent) => boolean];
managed
is generated frommanage
method:const managed = manage(store)
.func
is a function which readsmanaged
.result
is the result offunc()
.isTrigger
is a function which returnstrue
if anevent
will "trigger"func()
to have a different result.- when it returns true, most likely it's time to run
func()
again(because you will get a different result from last time).
- when it returns true, most likely it's time to run
When you invoke run(managed, func)
, func()
is invoked immediately.
You can subscribe to $(managed)
and filter the events using isTrigger
to get the trigger events (to run func()
again).
For a sample usage of run
, please check ./src/react.ts.
Another example is the implementation of the autoRun
utility method. You may find it in ./src/index.ts.
The signature of autoRun
is
function autoRun<T>(
managed: T,
func: () => void,
decorator?: (func: () => void) => () => void,
): { start: () => void; stop: () => void };
managed
is generated frommanage
method:const managed = manage(store)
.func
is a function which readsmanaged
.decorator
is a method to change run schedule offunc
, for example:func => _.debounce(func, 10, {leading: true, trailing: true})
start
andstop
is to start and stopautoRun
.
When you invoke start()
, func()
is invoked immediately.
func()
will be invoked automatically afterwards if there are trigger events from managed
which change the result of func()
.
Invoke stop
to stop autoRun
.
For sample usages of autoRun
, please check ./test/autoRun.spec.ts.
Transactions are used together with autoRun
.
When you put an object in transaction, changes to the object will not trigger autoRun
until the transaction ends.
import { $ } from 'manate';
const { start } = autoRun(managed, () => {
console.log(JSON.stringify(managed));
});
start(); // trigger `console.log`
$(managed).begin(); // start transaction
// perform changes to managed
// no matter how many changes you make, `console.log` will not be triggered
$(managed).commit(); // end transaction
// `console.log` will be triggered if there were changes
There could be multiple transactions at the same time. Transactions could be nested. A change will not trigger run until all enclosing transactions end.
const { start } = autoRun(managed, () => {
console.log(JSON.stringify(managed));
});
start(); // trigger `console.log`
$(managed).begin();
$(managed.a).begin();
// changes to `managed.a` will not trigger console.log until both transactions end
$(managed.a).commit();
$(managed).commit();
// `console.log` will be triggered if there were changes
For human-created plain objects, a reasonable maximum depth for recursive processing, ignoring circular references, typically ranges between 5 to 10 levels.
So this library by set the max depth to 10, if max depeth exceeded, an error will be thrown. In such case, you need to review the data to be managed, why is it so deeply nested, is it reasonable? Think about it: is the deelpy nested structure relevant to your business logic? should you manage it at all?
A real example is you try to manage a ReactElement
. React component instances contain deep, complex internal structures that reference other objects, functions, and potentially even themselves.
And you should not manage it at all. Instead, you should manage the state data used by the React component.
You may override the max depth by specify the second argument of the manage
function:
const store = manage(new Store(), 20); // explicitly set max depth to 20, if `Store` is by design a deeply nested data structure
- It doesn't manage built-in objects, such as
Set
,Map
andRTCPeerConnection
.
Recently I find manate is very similar to mobx:
import { manage } from 'manate'
is likeimport { observable } from 'mobx
import { auto } from 'manate/react
is likeimport { observer } from 'mobx-react-lite'
If I could realize the similarity 3 years ago, I might just use mobx instead.
For now, since manate is well developed and I am very happy with it, I will continue to use and maintain manate.