manate is short for "manage state". (pronunciation is close to "many-it") It is the most straightforward way to manage global state in React.
It supports TypeScript very well.
It is very straightforward to use. You don't need to learn any new concepts.
It allows you to maintain your app state in OOP style.
I am not saying that OOP style is the best practice for state management.
But if do want to code your state in OOP style, you should give this library a try.
Merely 200+ lines of code. There is no rocket science in this library.
yarn add manate
import { manage } from 'manate';
class Store {
count = 0;
increase() {
this.count += 1;
}
}
const store = manage(new Store());
import { auto } from 'manate/react';
const App = auto((props: { store: Store }) => {
const { store } = props;
return (
<Space>
<Button onClick={() => store.decrease()}>-</Button>
{store.count}
<Button onClick={() => store.increase()}>+</Button>
</Space>
);
});
It's fully compatible with React hooks.
import { manage } from 'manate';
import { ManateEvent } from 'manate/models';
class Store {}
const store = manage(new Store());
store.$e
is an EventEmitter which will emit events about read/write to store. You can subscribe to events:
store.$e.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:
class B {
public c = 1;
}
class A {
public b;
}
const a = new A();
const b = new B();
exclude(b);
a.b = b;
const ma = manage(a);
You may invoke the exlcude method before or after you manage the object:
class B {
public c = 1;
}
class A {
public b;
}
const a = new A();
const b = new B();
a.b = b;
const ma = manage(a);
exclude(ma.b);
For more details, please refer to the test cases in ./test/exclude.spec.ts.
The signature of run
is
function run<T>(managed: 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.$e
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: 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.
Well, actually it is possible and implementation is even shorter and simpler:
const auto = (render, props): JSX.Element | null => {
const [r, refresh] = useState(null);
useEffect(() => {
const managed = manage(props);
const { start, stop } = autoRun(managed, () => {
refresh(render());
});
start();
return () => {
stop();
releaseChildren(managed);
};
}, []);
return r;
};
Short answer: it's a bad practice to generate the result of render in useEffect
.
Big problem is:upstream components cannot invoke render
, because render
is inside useEffect
. So upstream useState
becomes useless.
So is there a way to run autoRun
out of useEffect
? Nope, because autoRun
by design is long running process and has side effects.
It's not a good idea to run autoRun
for every render
. run
is more suitable for this case.
According to the analysis above, if we want to support upstream component's useState
and strictMode
, we must run render
outside useEffect
.
However, run
requires a managed
object. Building such a managed
object has side effects. And when to dispose side effects? If we cannot answer this question, we cannot use run
.
After investigation, I found that useRef
can be used to dispose the side effects created in last render.
Ref: https://react.dev/reference/react/StrictMode
For functional components, StrictMode
will run useEffect
, then cleanup, then run useEffect
again.
For class components, StrictMode
will run componentDidMount
, then componentWillUnmount
, then componentDidMount
again.
If componentDidMount
is undefined, it will run neither. However, we cannot assume that componentDidMount
is undefined.
For both cases, we do need the dispose logic in componentWillUnmount
or useEffect
cleanup. If we want to support StrictMode
, we must write some setup code in useEffect
and componentDidMount
.
Otherwise, dispose without re-setup, it won't work.
For more details, please refer to ./src/react.ts
.
It's a bad idea. Because boolean has only two values.
If you want to trigger even number of re-render, the result is no re-render at all.
Because b === !!b
.
So we use useState(integer)
to re-render.
- every
emitter.on()
must have a correspondingemitter.off()
. Otherwise there will be memory leak.- you also don't have to
on
andoff
again and again. Sometimes you juston
and let it on until user explicit it request it to be off.
- you also don't have to
run
andautoRun
only support sync methods. for async methods, make sure that the async part is irrelevant because it won't be monitored.- rewrite some emitter.on to promise.
- the idea is great, but it will turn the library from sync to async, which will cause unexpected consequences.
React.render
,EventEmitter.on
,rxjs.observable.next
are all sync, there must be a good reason to stay with sync.
- Reference https://github.com/pmndrs/valtio
- This one is very similar to manate
- It only monitors
get
andset
of properties. It doesn't monitordelete
,has
andkeys
.- Because in 99.9% cases,
get
&set
are sufficient to monitor and manage data.
- Because in 99.9% cases,
- It doesn't monitor built-in objects, such as
Set
,Map
andRTCPeerConnection
. autoRun
doesn't monitor brand new properties. It only monitors existing properties.- workaround: pre-define all properties in the object. Event it doesn't have value yet, set it to
null
.null
is better thanundefined
becauseundefined
is not a valid value for JSON string.
- workaround: pre-define all properties in the object. Event it doesn't have value yet, set it to