manate
v2.0.0-beta.3
Published
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.
Downloads
1,101
Readme
manate
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.
Version 2.x
This is the branch for version 2.x. It is a complete rewrite of 1.x.
Why choose manate?
- 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!
Installation
yarn add manate
Manage the state
import { manage } from 'manate';
class Store {
count = 0;
increase() {
this.count += 1;
}
}
const store = manage(new Store());
Without class/function
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
React
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 state:
- update it directly:
store.count -= 1
- update it through a member function:
store.increase()
So, basically there is no restrictions. Just read/write as how you read/write a JavaScript object.
You don't need to pass managed state as props
In the sample above, we pass the store
state as a React props. But it is not necessary.
The following code works too:
import { auto } from 'manate/react';
import store from './store';
const App = auto(() => {
return (
<Space>
<Button
onClick={() => {
store.count -= 1;
}}
>
-
</Button>
{store.count}
<Button onClick={() => store.increase()}>+</Button>
</Space>
);
});
So the store could be a global variable instead of a React prop. It doesn't matter where the state is from, as long as it is managed by manate.
Read events & Write events
From manate's point of view, there are only two kinds of events: read events and write events.
Read events
Read events doesn't change data:
const obj = { prop: 1 };
console.log(obj.prop); // access a property
console.log('prop' in obj); // check a property existence
console.log(Object.keys(obj)); // list all the property keys
Write events
Write events change data:
const obj = {};
obj.prop = 1; // create a new property
obj.prop = 2; // update a property
delete obj.prop; // delete a property
Actions
All members functions in a managed object will be considered actions. What's the point of actions? actions batches the changes events.
For example, in an action, you change the state 100 times. But it will not trigger any reaction until the end of the action. And by the end of the action, depend on the final state, it will either trigger once or do not trigger.
Let's take React for example, in a method, you change the state 100 times. If the method is not an action, the react component will re-render 100 times. But if the method is an action, the react component will re-render at most 1 time.
Please refer to ./test/actions.spec.ts.
Reference but do not track
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 change event because `ma.b` is excluded.
You may invoke exclude
an object BEFORE it is managed.
For more details, please refer to the test cases in ./test/exclude.spec.ts.
Max Depth
For human-created JavaScript 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, console.warn('Max depth exceeded.');
will be executed and the manage
function will return early. Which means, state data deeper than 10 levels are not managed.
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?
You may override the max depth by specifying 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
autoRun
autoRun
is an utility method. It allows you to automatically run a code snippet if relevant state changes.
const runner = autoRun(() => {
console.log(store.count);
});
runner.start();
console.log(store.count);
will auto run every time store.count
is changed.
runner.start()
will run the code snippet immediately and start watching the state.
runner.stop
will stop watching the state.
runner.r
give you the latest return of the code snippet. It could be undefined
if it doesn't return anything.
run
run
is another utility method which powers autoRun
.
const [r, isTrigger] = run(() => {
return store.a + store.b;
});
writeEmitter.on((writeLog) => {
if (isTrigger(writeLog)) {
console.log(`run "store.a + store.b" now will generate a different result`);
}
});
So run
accept a funtion as argument. It returns [r, isTrigger]
. r
is the result of running the function argument.
isTrigger
is a function which check events emitted by writeEmitter
to tell if run the function again will generate a different result.
Map & Set
Map
and Set
work out-of-box, there is nothing special to mention here since it just works.
Please refer to
Computed
You can turn a function with no arguments or a getter into a computed function. Computed functions cache their results to avoid unnecessary recalculations.
When to use computed functions:
- The computation is expensive.
- The function is called multiple times.
- The state it depends on changes infrequently.
If any of these conditions aren't true, using computed may slow down your app instead of speeding it up.
const f = computed(() => {
return store.a + store.b;
});
Here, if store.a
and store.b
don't change, subsequent calls to f
will return the cached result.
However, this isn't a great example — store.a + store.b
is a simple computation, and using computed
here likely won't improve performance.
Built-in objects
Manate doesn't manage built-in objects, such as new RTCPeerConnection()
.
Because manate is a state management library. You're not supposed to hold your app state in those built-in objects.
Circular references
It is fine to have curcukar references in your state.
Refer to ./test/circular.spec.ts.
Similarity to MobX
Recently I find that 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.