The original address
mobx task manage: Loadx
Some time ago, I developed an efficiency tool, which can basically deal with relatively complex business scenarios. At present, relevant business codes have been online for several months and run well
introduce
The background,
-
The business is complicated and involves more up and down. Usually, interface functions are called everywhere. Later, loading and await are required, which is troublesome to think about
-
And if MOBx cannot perceive an await action, it must manually package the runInAction and make me spit blood
-
Again, an API usually has to be walked through with a try catch finally, which is cumbersome to write and can cause multiple rerenders if used incorrectly
-
Traditional writing has too much to manage, too little business focus, and too much fault tolerant code
Can’t do it, programmers are lazy… So there’s this Loadx productivity tool
Second, function points
-
Facilitate the organization and management of apis
-
Easy writing of error tolerance, finally, and subsequent action operations to focus on the business
-
Intelligent loading for concurrent and serial requests, unified rerender
-
You can initialize whether to load, pre-loading, and dynamic configuration
-
Support for Class Component and hooks
-
Convenient type prompt and various writing tolerance
The principle of
A,mobx.createAtom
-
The core of this is the createAtom API provided by Mobx
-
From my previous Mobx source code interpretation series, you should remember this API or the Atom class, which is the subscription-publish Atom for MOBx, on which all observableValue and so on are based
-
Two of the core methods are reportObserved and reportChanged
-
ReportObserved: Reports to subscribers when visiting self
-
ReportChanged: Report to subscribers when changing “myself”
- If we implemented a subscription-publish “publish mechanism” ourselves (mobx hijacked Render to trigger forceUpdate, of course), this would be the key to controlling loading or not
Second, the request of the stack
-
Maintain a queue of requests, which are pushed when there are interfaces, and unloaded when there are requests. Check the number of requests to see if it is currently loading
-
With Atom, heh heh
-
Request Length 0 -> 1: Start loading and call atom.reportChanged
-
Reqeust Length 1 -> 0: End loading, also call atom.reportchanged
-
Rerender is not called when length 1 -> n or n -> 1 is loaded
-
When getting the loading state: call atom.reportobserved
Iii. Handling of follow-up actions
-
If you have the try catch and runInAction, please help me to encapsulate them. Ok!
-
But after all, you’re executing in action, so return a function
What are you waiting for, a wank
A,Loadx
- The shelf
exportinterface LoadxConfig { name? : string; requests? :Promise<any>[];
}
export class Loadx {
name: string;
private atom: IAtom;
requests: Promise[] = [];
constructor(config? : LoadxConfig) {
const {
name = 'loadx',
requests,
} = config || {};
this.name = name;
this.atom = createAtom(name);
// Prepopulate request when new
requests && requests.forEach((p) = > this.load(p));
}
get loading() {
// When getting the status, report it up
this.atom.reportObserved();
return!!!!!this.requests.length;
}
// A single request is pushed
load(request: Promise) {
const { length: preLen } = this.requests;
const thenable = Promise.resolve(request);
this.requests.push(thenable);
/ / 0 - > 1
if(! preLen) {this.atom.reportChanged();
}
return thenable
.then(effect= > {
let res = effect;
runInAction(() = > {
// Handle the action function returned from the business
typeof effect === "function" && (res = effect.apply(this));
/ / out of the stack
this.finish(thenable);
});
return res;
})
.catch(err= > {
runInAction(() = > {
this.finish(thenable);
});
return Promise.reject(finalErr);
});
}
private finish(promise: Promise) {
this.requests.splice(this.requests.indexOf(promise), 1);
/ / 1 - > 0
if (!this.requests.length) {
this.atom.reportChanged(); }}}Copy the code
- use
class Store {
private loadx = new Loadx();
count = 0;
// To avoid direct external calls to loadX, register dependencies on atom at the React Render level
@computed
get loading() {
return this.loadx.loading;
}
_getCount() {
const count = await api();
// Return action fn
return () = > {
this.count = count; }}getCount() {
/ / associated
return this.loadx.load(action(this._getCount)); }}const store = new Store();
autorun(r= > {
console.log(store.loading, store.count);
});
store.getCount();
Copy the code
Two, adornment is true meaning
Why should I call loadX. load on my own initiative? Yes, encapsulated! It doesn’t feel good to call it like @action
- We use mobx god decorator: createDecoratorForEnhancer, unclear friends can see my previous articles, with the simplified version of the small example
class Loadx {
/** * Supports two decoration methods like mobx.action@action(ActionConfig) fn
* @action fn* /
static action = createPropDecorator(function (target, prop, descriptor, args) {
// @action fn() {}
// Note that this is a bind problem when you parse the call
if (descriptor) {
return {
configurable: true.enumerable: false.get() {
const fn = descriptor.value || (descriptor as any).initializer.call(this);
/ / the Object. DefineProperty
addHiddenProp(this, prop, createLoadxFn(fn, args[0].this));
return this[prop];
},
set(){}}; }else {
// @action fn = () => {}
Object.defineProperty(target, prop, {
configurable: true.enumerable: false.get() {},
set(fn) {
addHiddenProp(this, prop, createLoadxFn(fn, args[0].this)); }}); }}); }Copy the code
- Then look at the core of the decorator call:
createLoadxFn
exportinterface ActionConfig { loadx? : string | Loadx;// Associated store Loadxaction? : string;// Customize return action name
}
export function createLoadxFn(fn, config: ActionConfig = {}, context: any = null) {
const id = getUid();
return function (this: any, ... args: any[]) {
const that = context || this;
const { loadx: lName = "", action } = config;
// Find loadX instance from store instance according to name
const loadx:Loadx = that[lName];
// Execute the store method in the action and return a Promise
const req = runInAction(action, () = > fn.apply(that, args));
return loadx.load(req);
};
}
Copy the code
- use
class Store {
private loadx = new Loadx();
@Loadx.action
getUser() {
// Batch processing: release loading after promise. all and subsequent effect action
await getPermission();
const [name, age] = await Promise.all([nameApi(), ageApi()]);
// Return action fn
return () = > {
if (this.perm) {
this.name = name;
this.age = age;
}
}
}
@Loadx.action({
// Default error tolerance
// name: 'loadx'
})
getPermission = () = > {
const perm = await permApi();
return () = > {
this.perm = perm; }}}new Store().getUser();
Copy the code
- beautify
bind
Of course, if it’s a dynamic method, you can’t use a decorator, you can only use manual bind, so let’s beautify it
class Loadx {
bind<T extendsFnType>(fn: T, context? : any); bind<Textends FnType>(fn: T, config = {}, context: any = null
) {
/ / not config
if(! isPlainObject(config)) { context = config; config = {}; }returncreateLoadxFn(fn, { ... config,loadx: this }, context);
}
/ / use
class Store {
getCount = this.loadx.bind(this._getCount, this);
_getCount() {
const count = await api();
return () = > {
this.count = count; }}}const store = new Store();
store.getGender = store.loadx.bind(getGenderFromOtherPlace, store);
Copy the code
Third, improve the surrounding
try catch finally
-
Since catch has been removed, it cannot be written in action FN
-
That’s what we want, too, to be concise, so that we can focus on the business itself, rather than having to cram robustness into it. So, wrap it up!
exportinterface ActionConfig { loadx? : string | Loadx;/ / associated Loadxaction? : string;// action name
// Pass it in through configurationonError? :(err: any, ... originalArgs: any[]) = > void | Promise<void>; / / the error correctiononComplete? :(resOrErr? : any, ... originalArgs: any[]) = > void | Promise<void>; / / complete callback
}
export function createLoadxFn(fn config: ActionConfig = {}, context: any = null) {
return function (this: any, ... args: any[]) {
const that = context || this;
const {
loadx: lName = "",
action,
onComplete,
onError
} = config;
// ...
// Get the parameters from the decorator and hang them on the req
onError && (req._onError = onError.bind(that));
onComplete && (req._onComplete = onComplete.bind(that));
return loadx.load(req);
};
}
class Loadx {
load<T>(request: T): LoadxLoadType<T> {
/ / get the config
const { _onError, _onComplete } = request;
const { length: preLen } = this.requests;
// ...
return thenable
.then(effect= > {
let res = effect;
runInAction(() = > {
typeof effect === "function" && (res = effect.apply(this)); _onComplete && _onComplete(res, ... _args);this.finish(thenable);
});
return res;
})
.catch(err= > {
const finalErr = err;
runInAction(() = >{ _onError && _onError(finalErr, ... _args); _onComplete && _onComplete(finalErr, ... _args);this.finish(thenable);
});
return Promise.reject(finalErr); }); }}/ / use
class Store {
@Loadx.action({
onError(this: Store, err) {
console.log("onError".this, err);
},
onComplete(_resOrErr){}})getUser() {
// ...}}Copy the code
- control
loading
-
Sometimes I want to preload a state where loading is true, such as when I’m entering the initial loading of the page
-
Or IN some cases, I don’t want to listen to loading, and I’m setting it dynamically
exportinterface ActionConfig { loadx? : string | Loadx; action? : string;// Continue to configure parametersobserve? : boolean;// Set whether to listen for loading changes. The default is truepreload? : boolean | number; onError? :(err: any, ... originalArgs: any[]) = > void | Promise<void>; onComplete? :(resOrErr? : any, ... originalArgs: any[]) = > void | Promise<void>;
}
class Loadx {
setConfig(config: ActionConfig) {
// Change according to the input
if (has(config, "observe")) {
this.observe = config.observe;
}
if (has(config, "preload")) {
this.preload = config.preload;
if (!this.preload) return;
this.startPreload();
}
}
private startPreload() {
// Use setTimeout to simulate pre-sending a request
const loadReq = new Promise((resolve) = > {
setTimeout(resolve, this.preload);
this.preloadFn = () = > {
resolve();
this.preload = false;
this.preloadFn = null;
};
});
this.load(loadReq);
}
private finish(promise: Promise) {
this.requests.splice(this.requests.indexOf(promise), 1);
if (!this.requests.length) {
// Only when listening
this.observe && this.atom.reportChanged(); }}}/ / use
class Store {
@Loadx.action
getUser() {
this.loadx.setConfig({ observe: false });
const count = await api();
return () = > {
this.count = count; }}}Copy the code
- Intelligent type prompt, compatible writing method
-
Now the store methods are returned as functions, which can be confusing to use in components: Promise<() => void>
-
Some people are stubborn, and do not want to return to the Action FN, but must write their own runInAction
-
Again, someone wants to return actionFn and want to return the data after the await API. Arrangement!
class Store {
@Loadx.action
getUser() {
const perm = await getPermission();
const [name, age] = await Promise.all([nameApi(), ageApi()]);
// Return action fn
return () = > {
if (this.perm) {
this.name = name;
this.age = age;
}
}
}
@Loadx.action
getPermission = (flag = 1) = > {
const perm: boolean = await permApi(flag);
// 1: pure API
// return perm;
// 2: Why are you so stubborn
// runInAction(() => {
// this.perm = perm;
// })
/ / 3
return () = > {
this.perm = perm;
returnperm; }}}class Loadx {
load<T>(request: T): LoadxLoadType<T> {
// ...
return thenable
.then(effect= > {
let res = effect;
runInAction(() = > {
// The function returns the result of its execution
typeof effect === "function" && (res = effect.apply(this)); _onComplete && _onComplete(res, ... _args);this.finish(thenable);
});
return res;
})
.catch(err= > {
// ...}); }}Copy the code
Finally, one more wave of types. As for the specific types, it is not the focus of this article, I will separate the article in the Types Skills Summary, please look forward to it
export type UnFnReturn<T> = T extends(... args: any[]) => infer R ? R : T;export type LoadxLoadFnType<T> = T extends(... args: any[]) =>Promise<infer R> | Generator<any, infer R>
? (. args: Parameters
) = > Promise<UnFnReturn<R>>
: T;
// Restrict store methods that return Promise
export type LoadxStore<T extends Record<string, any>> = {
[K in keyof T]: LoadxLoadFnType<T[K]>;
};
/ / use
@observer
class CC extends Component<{store: LoadxStore<Store>}> {
render() {
const { getPermission } = this.props.store;
// type:
// const getPermission: (flag: string) => Promise<boolean>
return <div></div>}}Copy the code
Fourth, the hooks
Now that we’re done, hooks are introduced, which are useLocalStore, just exposing the API
export function useLoadx(initializer: (source: P & { loadx: Loadx }) => T, loadxConfig? : ActionConfig, current? : P) :LoadxLocaleStore<T> {
// create loadx for store
const loadx = useMemo(() = > new Loadx(loadxConfig), []);
current && (current.loadx = loadx);
const store = useLocalStore(initializer, current);
return {
store,
loading: loadx.loading,
bind: (fn: T, config = {}, context: any = null) = > {
/ / not config
if(! isPlainObject(config)) { context = config; config = {}; }returncreateLoadxFn( fn, { ... config, loadx }, context || store ); },setConfig: (config) = > loadx.setConfig(config)
};
}
/ / use
const Fc = () = > {
const { store, loading, bind } = useLoadx(() = > ({
data: { name: "empty"}}));const getDataWithLoading = bind(async() = > {const res = await wait(1000, { name: 'lawler'.age: 20 });
return () = > {
store.data = res;
};
});
const { data } = store;
return useObserver(() = > (
<div>
<div>
{`loading: ${loading}, data: ${data.name}`}
</div>
<button onClick={getDataWithLoading}>FC loadx</button>
</div>
));
};
export default observer(Fc);
Copy the code
The last
-
Loadx source Loadx source
-
If the response is good, you can consider open source (yes, install 13 only), and welcome folk improvement
-
In addition, I did not find out until I finished writing this util that there is a big guy on Github who has completed a similar tool mox-Task. After checking it out, it is completely different from his source code and use gesture, but it can also be used as a reference
-
Like small partners, remember to leave your small ❤️ oh ~