@tylerlong/use-proxy
v1.3.1
Published
This project has been renamed to [manate](https://www.npmjs.com/package/manate).
Downloads
17
Readme
deprecated
This project has been renamed to manate.
useProxy
The name useProxy
was inspired by React useState.
Just like useState
, it is mainly designed to work with React applications.
useProxy
is the successor of SubX, which is similar to MobX.
What's the value of useProxy
?
It allows you to maintain your app state in OOP style.
I am not saying that OOP style is the best practice for React development.
But if do want to code your React app in OOP style, you should give this library a try.
It supports TypeScript very well.
Demo application
Installation
yarn add @tylerlong/use-proxy
Usage
import { useProxy } from '@tylerlong/use-proxy';
import { Component } from '@tylerlong/use-proxy/lib/react';
class Store {
count = 0;
increase() {
this.count += 1;
}
}
const store = useProxy(new Store());
class App extends Component<{ store: Store }> {
render() {
const store = this.props.store;
return (
<div>
<span>{store.count}</span>
<button onClick={() => store.increase()}>+</button>
</div>
);
}
}
Functional React Component & React Hooks
import { auto } from '@tylerlong/use-proxy/lib/react';
const App = (props: { store: Store }) => {
const { store } = props;
const render = () => (
<Space>
<Button onClick={() => store.decrease()}>-</Button>
{store.count}
<Button onClick={() => store.increase()}>+</Button>
</Space>
);
return auto(render, props);
};
It's fully compatible with useState
and useEffect
.
A fully working demo is here.
Event Emitter
import { useProxy } from '@tylerlong/use-proxy';
import { ProxyEvent } from '@tylerlong/use-proxy/lib/models';
class Store {}
const store = useProxy(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', (event: ProxyEvent) => {
// do something with event
});
Utility methods
run
The signature of run
is
function run<T>(proxy: ProxyType<T>, func: Function): [result: any, isTrigger: (event: ProxyEvent) => boolean];
proxy
is generated fromuseProxy
method:const proxy = useProxy(store)
.func
is a function which readsproxy
.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(proxy, func)
, func()
is invoked immediately.
You can subscribe to proxy.$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.
autoRun
The signature of autoRun
is
function autoRun<T>(
proxy: ProxyType<T>,
func: () => void,
decorator?: (func: () => void) => () => void,
): { start: () => void; stop: () => void };
proxy
is generated fromuseProxy
method:const proxy = useProxy(store)
.func
is a function which readsproxy
.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 proxy
which change the result of func()
.
Invoke stop
to stop autoRun
.
For sample usages of autoRun
, please check ./test/autoRun.spec.ts.
Question #1: why not use autoRun
to support React hooks?
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 proxy = useProxy(props);
const { start, stop } = autoRun(proxy, () => {
refresh(render());
});
start();
return () => {
stop();
releaseChildren(proxy);
};
}, []);
return r;
};
Big problem is:https://github.com/tylerlong/use-proxy-react-demo/blob/03ca533592a78a446d3688274c7b47059644dda3/src/index.tsx。
Upstream components cannot invoke render
, because render
is inside useEffect
. So upstream useState
becomes useless。
Another minor issue:
But there is an issue: React StrictMode
doesn't works for us any more.
Because StrictMode will try to do double rendering. However, we only invoke render
in useEffect
.
So double rendering will not invoke render
at all, thus it cannot help us to detect non-pure function issues.
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.
Question #2: why use run
to support React hooks?
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 proxy
object. Building such a proxy
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.
Todo
- Rename to "manate": manage + state
- allow to
import {auto} from 'manate/react'
instead ofimport {auto} from '@tylerlong/use-proxy/lib/react'
- pretty hard
Development Notes
- 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.
Known limitations
- 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 work with some built-in objects, such as
Set
&Map
. - It desn't work with native objects, such as
window.speechSynthesis.getVoices()
. 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
- no circular references, otherwise
Uncaught RangeError: Maximum call stack size exceeded